diff --git a/CHANGES.md b/CHANGES.md index 3eec41940..86a4d722e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,11 @@ limitations under the License. # Noteworthy Changes for 3.0.0 (TBD) +## New features + +- New `libs/jansson_ext` library providing JSON Schema draft-7 validation, + JSON Pointer (RFC 6901) and JSON Patch (RFC 6902) support, built on Jansson. + ## Backwards incompatible changes - Deployment Admin bundle has been removed and is no longer supported. diff --git a/cmake/celix_project/CodeCoverage.cmake b/cmake/celix_project/CodeCoverage.cmake index 7615b38fc..ba1aed071 100644 --- a/cmake/celix_project/CodeCoverage.cmake +++ b/cmake/celix_project/CodeCoverage.cmake @@ -42,6 +42,7 @@ # 4. Added "mock" to exclude list for coverage results # 5. Removed HTML generation from the coverage setup function # 6. Removed unneeded Cobertura function +# 7. Version-gated --ignore-errors flags for lcov 2.x # # Option to enable/disable coverage @@ -60,6 +61,27 @@ IF(ENABLE_CODE_COVERAGE) MESSAGE(FATAL_ERROR "gcov not found! Aborting...") ENDIF() # NOT GCOV_PATH + # lcov 2.x errors on gcov end-line inconsistencies for functions sharing a + # start line (e.g. gtest TEST_F bodies and their synthesized ctor/dtors) and + # on unused exclude patterns; 1.x neither has these checks nor accepts the + # corresponding --ignore-errors categories (unknown categories are fatal in + # all versions), so the flags are version-gated. + execute_process(COMMAND ${LCOV_PATH} --version + OUTPUT_VARIABLE LCOV_VERSION_OUTPUT + ERROR_QUIET) + set(LCOV_MAJOR_VERSION 0) + if (LCOV_VERSION_OUTPUT MATCHES "LCOV version ([0-9]+)\\.([0-9]+)") + set(LCOV_MAJOR_VERSION ${CMAKE_MATCH_1}) + endif () + if (LCOV_MAJOR_VERSION GREATER_EQUAL 2) + # mismatch = lcov 2.0, inconsistent = 2.1+ (renamed); both accepted on all 2.x + set(LCOV_CAPTURE_IGNORE_ERRORS --ignore-errors mismatch,inconsistent) + set(LCOV_REMOVE_IGNORE_ERRORS --ignore-errors unused) + else () + set(LCOV_CAPTURE_IGNORE_ERRORS "") + set(LCOV_REMOVE_IGNORE_ERRORS "") + endif () + #IF(NOT CMAKE_COMPILER_IS_GNUCXX) # MESSAGE(FATAL_ERROR "Compiler is not GNU gcc! Aborting...") #ENDIF() # NOT CMAKE_COMPILER_IS_GNUCXX @@ -134,8 +156,8 @@ function (setup_target_for_coverage) # Capturing lcov counters and generating report COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/coverage - COMMAND ${LCOV_PATH} --directory ${COVERAGE_SCAN_DIR} --capture --output-file ${OUTPUT_FILE} - COMMAND ${LCOV_PATH} --remove ${OUTPUT_FILE} '**/error_injector/*' '**/mock/*' '**/.conan/*' '**/test/*' '**/gtest/*' '**/tst/*' '**/celix/gen/*' '**/googletest_project/*' '**/glog/*' '/usr/*' --output-file ${OUTPUT_FILE}.cleaned + COMMAND ${LCOV_PATH} --directory ${COVERAGE_SCAN_DIR} --capture ${LCOV_CAPTURE_IGNORE_ERRORS} --output-file ${OUTPUT_FILE} + COMMAND ${LCOV_PATH} --remove ${OUTPUT_FILE} '**/error_injector/*' '**/mock/*' '**/.conan/*' '**/test/*' '**/gtest/*' '**/tst/*' '**/celix/gen/*' '**/googletest_project/*' '**/glog/*' '/usr/*' ${LCOV_REMOVE_IGNORE_ERRORS} --output-file ${OUTPUT_FILE}.cleaned #test dependencies, so that test is runned DEPENDENCIES ${TEST_TARGET_NAME} diff --git a/conanfile.py b/conanfile.py index 711b96199..7e9c481a1 100644 --- a/conanfile.py +++ b/conanfile.py @@ -80,6 +80,8 @@ class CelixConan(ConanFile): "build_experimental": False, "build_celix_dfi": False, "build_framework": False, + "build_jansson_ext": False, + "build_jansson_ext_examples": False, "build_rcm": False, "build_utils": False, "build_event_admin": False, @@ -193,6 +195,7 @@ def _get_dependency_option_value(self, dep_name, option_name): ValidationRule(self.options.build_celix_dfi, 'jansson', "shared", True, 'build_celix_dfi=True'), ValidationRule(self.options.build_celix_etcdlib, 'jansson', "shared", True, 'build_celix_etcdlib=True'), ValidationRule(self.options.build_event_admin_remote_provider_mqtt, 'jansson', "shared", True, 'build_event_admin_remote_provider_mqtt=True'), + ValidationRule(self.options.build_jansson_ext, 'jansson', "shared", True, 'build_jansson_ext=True'), ValidationRule(self.options.build_event_admin_remote_provider_mqtt and self.options.enable_testing, "mosquitto", "broker", True, "build_event_admin_remote_provider_mqtt=True and enable_testing=True"), ] @@ -343,6 +346,12 @@ def configure(self): if options["build_rcm"]: options["build_utils"] = True + if options["build_jansson_ext_examples"]: + options["build_jansson_ext"] = True + + if options["build_jansson_ext"]: + options["build_utils"] = True + if options["build_launcher"]: options["build_framework"] = True @@ -388,13 +397,13 @@ def requirements(self): self.requires("civetweb/1.16") if self.options.build_celix_dfi: self.requires("libffi/[>=3.2.1 <4.0.0]") - if self.options.build_utils or self.options.build_celix_dfi or self.options.build_celix_etcdlib or self.options.build_event_admin_remote_provider_mqtt: + if self.options.build_utils or self.options.build_celix_dfi or self.options.build_celix_etcdlib or self.options.build_event_admin_remote_provider_mqtt or self.options.build_jansson_ext: self.requires("jansson/[>=2.12 <3.0.0]") if self.options.build_rsa_discovery_zeroconf: # TODO: To be replaced with mdnsresponder/1790.80.10, resolve some problems of mdnsresponder # https://github.com/conan-io/conan-center-index/pull/16254 self.requires("mdnsresponder/1310.140.1") - self.requires("openssl/[>=3.2.0]", override=True) + self.requires("openssl/[>=3.2.0 <4.0.0]", override=True) # Fix zlib to 1.3.1, 'libzip/1.10.1' and 'libcurl/7.64.1' requires different zlib versions causing conflicts self.requires("zlib/1.3.1", override=True) if self.options.build_event_admin_remote_provider_mqtt: @@ -455,3 +464,8 @@ def package_info(self): self.cpp_info.bindirs = ["bin", os.path.join("share", self.name, "bundles")] self.cpp_info.builddirs.append(os.path.join("lib", "cmake", "Celix")) self.cpp_info.set_property("cmake_find_mode", "none") + # Celix is always built as shared libraries by its CMake build system, so consumers + # need the lib dir in the run environment to find the libraries at runtime. + self.runenv_info.prepend_path("LD_LIBRARY_PATH", os.path.join(self.package_folder, "lib")) + if self.settings.os == "Macos": + self.runenv_info.prepend_path("DYLD_LIBRARY_PATH", os.path.join(self.package_folder, "lib")) diff --git a/libs/CMakeLists.txt b/libs/CMakeLists.txt index 8da61c273..92da705a6 100644 --- a/libs/CMakeLists.txt +++ b/libs/CMakeLists.txt @@ -19,6 +19,7 @@ #(e.g. no dependency on celix framework add_subdirectory(utils) add_subdirectory(rcm) +add_subdirectory(jansson_ext) add_subdirectory(dfi) add_subdirectory(etcdlib) add_subdirectory(promises) diff --git a/libs/error_injector/jansson/CMakeLists.txt b/libs/error_injector/jansson/CMakeLists.txt index 4596350b0..f99b662eb 100644 --- a/libs/error_injector/jansson/CMakeLists.txt +++ b/libs/error_injector/jansson/CMakeLists.txt @@ -32,6 +32,8 @@ target_link_options(jansson_ei INTERFACE LINKER:--wrap,json_object_set_new LINKER:--wrap,json_array LINKER:--wrap,json_array_append_new + LINKER:--wrap,json_array_set_new + LINKER:--wrap,json_array_insert_new LINKER:--wrap,json_integer LINKER:--wrap,json_string LINKER:--wrap,json_real @@ -41,5 +43,6 @@ target_link_options(jansson_ei INTERFACE LINKER:--wrap,json_pack_ex LINKER:--wrap,json_null LINKER:--wrap,json_loads + LINKER:--wrap,json_deep_copy ) add_library(Celix::jansson_ei ALIAS jansson_ei) diff --git a/libs/error_injector/jansson/include/jansson_ei.h b/libs/error_injector/jansson/include/jansson_ei.h index feb9f59ef..d86b4aac3 100644 --- a/libs/error_injector/jansson/include/jansson_ei.h +++ b/libs/error_injector/jansson/include/jansson_ei.h @@ -31,6 +31,8 @@ CELIX_EI_DECLARE(json_object, json_t*); CELIX_EI_DECLARE(json_object_set_new, int); CELIX_EI_DECLARE(json_array, json_t*); CELIX_EI_DECLARE(json_array_append_new, int); +CELIX_EI_DECLARE(json_array_set_new, int); +CELIX_EI_DECLARE(json_array_insert_new, int); CELIX_EI_DECLARE(json_integer, json_t*); CELIX_EI_DECLARE(json_string, json_t*); CELIX_EI_DECLARE(json_real, json_t*); @@ -40,6 +42,7 @@ CELIX_EI_DECLARE(json_dumpf, int); CELIX_EI_DECLARE(json_pack_ex, json_t*); CELIX_EI_DECLARE(json_null, json_t*); CELIX_EI_DECLARE(json_loads, json_t*); +CELIX_EI_DECLARE(json_deep_copy, json_t*); #ifdef __cplusplus } diff --git a/libs/error_injector/jansson/src/jansson_ei.cc b/libs/error_injector/jansson/src/jansson_ei.cc index 0328d40e9..e9e95e98f 100644 --- a/libs/error_injector/jansson/src/jansson_ei.cc +++ b/libs/error_injector/jansson/src/jansson_ei.cc @@ -67,6 +67,22 @@ int __wrap_json_array_append_new(json_t* array, json_t* value) { return __real_json_array_append_new(array, celix_steal_ptr(val)); } +int __real_json_array_set_new(json_t* array, size_t index, json_t* value); +CELIX_EI_DEFINE(json_array_set_new, int) +int __wrap_json_array_set_new(json_t* array, size_t index, json_t* value) { + json_auto_t* val = value; + CELIX_EI_IMPL(json_array_set_new); + return __real_json_array_set_new(array, index, celix_steal_ptr(val)); +} + +int __real_json_array_insert_new(json_t* array, size_t index, json_t* value); +CELIX_EI_DEFINE(json_array_insert_new, int) +int __wrap_json_array_insert_new(json_t* array, size_t index, json_t* value) { + json_auto_t* val = value; + CELIX_EI_IMPL(json_array_insert_new); + return __real_json_array_insert_new(array, index, celix_steal_ptr(val)); +} + json_t* __real_json_integer(json_int_t value); CELIX_EI_DEFINE(json_integer, json_t*) json_t* __wrap_json_integer(json_int_t value) { @@ -138,4 +154,11 @@ json_t* __wrap_json_loads(const char* input, size_t flags, json_error_t* error) return __real_json_loads(input, flags, error); } +json_t* __real_json_deep_copy(const json_t* value); +CELIX_EI_DEFINE(json_deep_copy, json_t*) +json_t* __wrap_json_deep_copy(const json_t* value) { + CELIX_EI_IMPL(json_deep_copy); + return __real_json_deep_copy(value); +} + } \ No newline at end of file diff --git a/libs/error_injector/stdio/CMakeLists.txt b/libs/error_injector/stdio/CMakeLists.txt index 2e53d8099..3572b81e0 100644 --- a/libs/error_injector/stdio/CMakeLists.txt +++ b/libs/error_injector/stdio/CMakeLists.txt @@ -33,5 +33,6 @@ target_link_options(stdio_ei INTERFACE LINKER:--wrap,fclose LINKER:--wrap,fgetc LINKER:--wrap,fmemopen + LINKER:--wrap,vsnprintf ) add_library(Celix::stdio_ei ALIAS stdio_ei) diff --git a/libs/error_injector/stdio/include/stdio_ei.h b/libs/error_injector/stdio/include/stdio_ei.h index b5be1e311..a80cb174f 100644 --- a/libs/error_injector/stdio/include/stdio_ei.h +++ b/libs/error_injector/stdio/include/stdio_ei.h @@ -50,6 +50,8 @@ CELIX_EI_DECLARE(fgetc, int); CELIX_EI_DECLARE(fmemopen, FILE*); +CELIX_EI_DECLARE(vsnprintf, int); + #ifdef __cplusplus } #endif diff --git a/libs/error_injector/stdio/src/stdio_ei.cc b/libs/error_injector/stdio/src/stdio_ei.cc index f9bc89d52..1d4004df2 100644 --- a/libs/error_injector/stdio/src/stdio_ei.cc +++ b/libs/error_injector/stdio/src/stdio_ei.cc @@ -18,6 +18,7 @@ */ #include +#include #include "stdio_ei.h" extern "C" { @@ -124,4 +125,13 @@ FILE* __wrap_fmemopen(void* __s, size_t __len, const char* __modes) { return __real_fmemopen(__s, __len, __modes); } -} \ No newline at end of file +int __real_vsnprintf(char* __restrict __s, size_t __maxlen, const char* __restrict __format, va_list __arg); +CELIX_EI_DEFINE(vsnprintf, int) +int __wrap_vsnprintf(char* __restrict __s, size_t __maxlen, const char* __restrict __format, va_list __arg) { + errno = EOVERFLOW; //glibc sets this when the formatted output length exceeds INT_MAX + CELIX_EI_IMPL(vsnprintf); + errno = 0; + return __real_vsnprintf(__s, __maxlen, __format, __arg); +} + +} diff --git a/libs/jansson_ext/CMakeLists.txt b/libs/jansson_ext/CMakeLists.txt new file mode 100644 index 000000000..cec7620ea --- /dev/null +++ b/libs/jansson_ext/CMakeLists.txt @@ -0,0 +1,83 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +celix_subproject(JANSSON_EXT "Option to enable building the Jansson extension library (JSON Schema validation, JSON Pointer and JSON Patch)" OFF) +if (JANSSON_EXT) + find_package(jansson REQUIRED) + + set(JANSSON_EXT_SOURCES + src/celix_jansson_schema.c + src/celix_jansson_pointer.c + src/celix_json_patch.c + src/celix_json_merge_patch.c + src/celix_jansson_uri.c + src/celix_string_format_check.c + src/celix_smtp_address_validator.c + src/celix_util.c + ) + + add_library(jansson_ext SHARED ${JANSSON_EXT_SOURCES}) + set_target_properties(jansson_ext PROPERTIES + OUTPUT_NAME "celix_jansson_ext" + VERSION 0.1.0 + SOVERSION 0) + celix_target_hide_symbols(jansson_ext) + target_link_libraries(jansson_ext + PUBLIC jansson::jansson Threads::Threads Celix::utils + PRIVATE m) + target_include_directories(jansson_ext + PRIVATE src + PUBLIC + $ + $) + generate_export_header(jansson_ext + BASE_NAME CELIX_JANSSON_EXT + EXPORT_FILE_NAME "${CMAKE_CURRENT_BINARY_DIR}/celix/gen/includes/jansson_ext/celix_jansson_ext_export.h") + add_library(Celix::jansson_ext ALIAS jansson_ext) + + # Install (always, no option gate) + install(TARGETS jansson_ext + EXPORT celix + DESTINATION ${CMAKE_INSTALL_LIBDIR} + COMPONENT framework + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/celix/jansson_ext) + install(DIRECTORY include/ + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/celix/jansson_ext + COMPONENT framework) + install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/celix/gen/includes/jansson_ext/ + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/celix/jansson_ext + COMPONENT framework) + + if (ENABLE_TESTING) + add_library(jansson_ext_cut STATIC ${JANSSON_EXT_SOURCES}) + target_link_libraries(jansson_ext_cut + PUBLIC jansson::jansson Threads::Threads Celix::utils + PRIVATE m) + target_include_directories(jansson_ext_cut + PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/include + ${CMAKE_CURRENT_LIST_DIR}/src + ${CMAKE_CURRENT_BINARY_DIR}/celix/gen/includes/jansson_ext) + target_compile_definitions(jansson_ext_cut PUBLIC CELIX_JANSSON_EXT_STATIC_DEFINE) + add_subdirectory(gtest) + endif () + + celix_subproject(JANSSON_EXT_EXAMPLES "Option to enable building the jansson extension examples" OFF) + if (JANSSON_EXT_EXAMPLES) + add_subdirectory(example) + endif () +endif () diff --git a/libs/jansson_ext/README.md b/libs/jansson_ext/README.md new file mode 100644 index 000000000..53f8c6e27 --- /dev/null +++ b/libs/jansson_ext/README.md @@ -0,0 +1,53 @@ +# Jansson Extension Library + +The Jansson Extension (`jansson_ext`) library provides JSON Schema validation, +JSON Pointer, JSON Patch, and JSON Merge Patch functionality built on top of the +[Jansson](https://github.com/akheron/jansson) C library. + +## Features + +- **JSON Schema Draft-7 Validation** — Compile schemas into an internal object + tree for fast repeated validation. Supports all draft-7 keywords including + `$ref`, `allOf`/`anyOf`/`oneOf`, `if`/`then`/`else`, `format`, default + values, and more. +- **JSON Pointer (RFC 6901)** — Parse, build, and resolve JSON Pointers against + JSON documents. Supports stack-allocation of pointer objects for zero-overhead + path manipulation. +- **JSON Patch (RFC 6902)** — Build and apply JSON Patch documents for + programmatic JSON transformations. +- **JSON Merge Patch (RFC 7396)** — Apply merge patch documents, where a null + member removes the corresponding member and a non-object patch replaces the + whole document. + +## API + +Public headers are in the `include/` directory: + +- [celix_jansson_schema.h](include/celix_jansson_schema.h) — JSON Schema validation +- [celix_jansson_pointer.h](include/celix_jansson_pointer.h) — JSON Pointer operations +- [celix_json_patch.h](include/celix_json_patch.h) — JSON Patch builder +- [celix_json_merge_patch.h](include/celix_json_merge_patch.h) — JSON Merge Patch application + +## Dependencies + +- [Jansson](https://github.com/akheron/jansson) `>= 2.12` +- POSIX Threads (`pthread`) + +## Building + +The library is built as part of Apache Celix. Enable it with: + +```bash +cmake -DBUILD_JANSSON_EXT=ON -S . -B build +cmake --build build +``` + +With Conan: + +```bash +conan create . -o build_jansson_ext=True --build=missing +``` + +## License + +Licensed under the Apache License, Version 2.0. diff --git a/libs/jansson_ext/example/CMakeLists.txt b/libs/jansson_ext/example/CMakeLists.txt new file mode 100644 index 000000000..f64131cb4 --- /dev/null +++ b/libs/jansson_ext/example/CMakeLists.txt @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +add_executable(json_schema_validate json_schema_validate.c) +target_link_libraries(json_schema_validate PRIVATE Celix::jansson_ext) diff --git a/libs/jansson_ext/example/json_schema_validate.c b/libs/jansson_ext/example/json_schema_validate.c new file mode 100644 index 000000000..3d4a384e8 --- /dev/null +++ b/libs/jansson_ext/example/json_schema_validate.c @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "celix_jansson_schema.h" +#include "celix_json_patch.h" +#include +#include +#include + +/* + * Schema loader callback: reads external schema files from the current + * working directory based on the URI path. + */ +static int file_loader(const char* uri, json_t** schema_out, void* user_data) { + (void)user_data; + + /* Serve the built-in draft-7 meta-schema */ + if (strcmp(uri, "http://json-schema.org/draft-07/schema") == 0 || + strcmp(uri, "http://json-schema.org/draft-07/schema#") == 0) { + *schema_out = celix_jansson_schema_draft7_meta_schema(); + if (!*schema_out) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + return CELIX_JANSSON_SCHEMA_OK; + } + + /* Try to load from disk: ./ */ + char path[4096]; + const char* uri_path = uri; + + /* Strip scheme://authority prefix if present */ + const char* scheme_end = strstr(uri, "://"); + if (scheme_end) { + const char* slash = strchr(scheme_end + 3, '/'); + if (slash) + uri_path = slash; + else + uri_path = "/"; + } + + snprintf(path, sizeof(path), ".%s", uri_path); + + json_error_t jerr; + *schema_out = json_load_file(path, 0, &jerr); + if (!*schema_out) { + fprintf(stderr, "Error loading schema '%s': %s\n", path, jerr.text); + return CELIX_JANSSON_SCHEMA_ERROR_LOADER; + } + + return CELIX_JANSSON_SCHEMA_OK; +} + +static void print_error(const char* ptr, json_t* instance, const char* msg, void* user_data) { + (void)user_data; + char* inst_str = json_dumps(instance, JSON_ENCODE_ANY); + fprintf(stderr, "ERROR: '%s' - '%s': %s\n", ptr, inst_str ? inst_str : "?", msg); + free(inst_str); +} + +int main(int argc, char** argv) { + if (argc < 2) { + fprintf(stderr, "Usage: %s < document.json\n", argv[0]); + return 2; + } + + /* Load schema from file */ + json_error_t jerr; + json_t* schema = json_load_file(argv[1], 0, &jerr); + if (!schema) { + fprintf(stderr, "Error loading schema from '%s': %s\n", argv[1], jerr.text); + return 1; + } + + /* Create validator with file loader and built-in format checker */ + celix_jansson_schema_validator_t* validator = celix_jansson_schema_validator_create( + file_loader, NULL, celix_jansson_schema_default_format_check, NULL, NULL, NULL); + + if (!validator) { + fprintf(stderr, "Failed to create validator\n"); + json_decref(schema); + return 1; + } + + /* Compile the schema */ + char* errmsg = NULL; + int rc = celix_jansson_schema_set_root_schema(validator, schema, &errmsg); + json_decref(schema); + + if (rc != CELIX_JANSSON_SCHEMA_OK) { + fprintf(stderr, "Schema compilation error: %s\n", errmsg ? errmsg : celix_jansson_schema_strerror(rc)); + free(errmsg); + celix_jansson_schema_validator_destroy(validator); + return 1; + } + + /* Read document from stdin */ + json_t* instance = json_loadf(stdin, 0, &jerr); + if (!instance) { + fprintf(stderr, "Error parsing input: %s\n", jerr.text); + celix_jansson_schema_validator_destroy(validator); + return 1; + } + + /* Validate */ + json_t* patch = NULL; + int errors = celix_jansson_schema_validate(validator, instance, print_error, NULL, &patch); + + if (errors == 0) { + fprintf(stderr, "Document is valid.\n"); + } else { + fprintf(stderr, "Document is invalid (%d errors).\n", errors); + } + + /* Print defaults patch if any */ + if (patch && json_array_size(patch) > 0) { + char* patch_str = json_dumps(patch, JSON_INDENT(2)); + fprintf(stderr, "Default values patch:\n%s\n", patch_str ? patch_str : ""); + free(patch_str); + + /* Apply the patch to get a document with defaults filled in */ + json_t* filled = celix_json_patch_apply(instance, patch); + if (filled) { + char* filled_str = json_dumps(filled, JSON_INDENT(2)); + fprintf(stderr, "Document with defaults:\n%s\n", filled_str ? filled_str : ""); + free(filled_str); + json_decref(filled); + } + } + json_decref(patch); + + json_decref(instance); + celix_jansson_schema_validator_destroy(validator); + + return errors > 0 ? 1 : 0; +} diff --git a/libs/jansson_ext/gtest/CMakeLists.txt b/libs/jansson_ext/gtest/CMakeLists.txt new file mode 100644 index 000000000..5127f2c39 --- /dev/null +++ b/libs/jansson_ext/gtest/CMakeLists.txt @@ -0,0 +1,75 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +set(CMAKE_CXX_STANDARD 17) + +set(TEST_SOURCES + src/test_smoke.cpp + src/test_uri.cpp + src/test_errors.cpp + src/test_format_check.cpp + src/test_ref.cpp + src/test_defaults.cpp + src/test_combinations.cpp + src/test_suite.cpp + src/test_path.cpp + src/test_pointer.cpp + src/test_patch.cpp + src/test_json_patch.cpp + src/test_merge_patch.cpp + src/test_validate_uri.cpp + src/test_abort.cpp + src/test_format_check_extended.cpp + src/test_schema_keywords.cpp + src/test_uri_extended.cpp + src/test_util.cpp +) + +foreach(T ${TEST_SOURCES}) + get_filename_component(NAME ${T} NAME_WE) + add_executable(${NAME} ${T}) + target_link_libraries(${NAME} PRIVATE jansson_ext_cut GTest::gtest GTest::gtest_main) + target_include_directories(${NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_compile_definitions(${NAME} + PRIVATE TEST_SUITE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/src/JSON-Schema-Test-Suite") + add_test(NAME ${NAME} COMMAND ${NAME}) + setup_target_for_coverage(${NAME} SCAN_DIR ..) +endforeach() + +if (EI_TESTS) + # Note: the error-injection tests are separated from the regular tests because + # they link the error-injector wrappers (--wrap) and set injection expectations + # that must not leak into the normal test run. + add_executable(test_jansson_ext_with_ei + src/ErrorInjectionTestSuite.cc + src/SchemaErrorInjectionTestSuite.cc + src/MergePatchErrorInjectionTestSuite.cc + ) + target_link_libraries(test_jansson_ext_with_ei PRIVATE + jansson_ext_cut + Celix::malloc_ei + Celix::string_ei + Celix::stdio_ei + Celix::asprintf_ei + Celix::jansson_ei + Celix::string_hash_map_ei + GTest::gtest GTest::gtest_main + ) + target_include_directories(test_jansson_ext_with_ei PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + add_test(NAME test_jansson_ext_with_ei COMMAND test_jansson_ext_with_ei) + setup_target_for_coverage(test_jansson_ext_with_ei SCAN_DIR ..) +endif () diff --git a/libs/jansson_ext/gtest/src/ErrorInjectionTestSuite.cc b/libs/jansson_ext/gtest/src/ErrorInjectionTestSuite.cc new file mode 100644 index 000000000..69aac7de4 --- /dev/null +++ b/libs/jansson_ext/gtest/src/ErrorInjectionTestSuite.cc @@ -0,0 +1,1350 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include + +#include "celix_cleanup.h" +#include "celix_jansson_pointer.h" +#include "celix_jansson_uri.h" +#include "celix_json_patch.h" +#include "celix_util.h" +#include "jansson_ei.h" +#include "malloc_ei.h" +#include "stdio_ei.h" +#include "string_ei.h" + +/** + * Error-injection tests for the OOM (out-of-memory) handling paths of + * celix_util, celix_jansson_pointer, celix_jansson_uri and celix_json_patch. + * + * Every allocation-failure branch (if (!ptr) return ...) is exercised by + * injecting a NULL return for the matching allocator with an exact caller + * match. The injected caller address is the function that (directly or, via + * `level`, indirectly) calls the wrapped allocator. + */ +class JanssonExtErrorInjectionTestSuite : public ::testing::Test { +public: + ~JanssonExtErrorInjectionTestSuite() noexcept override { + celix_ei_expect_malloc(nullptr, 0, nullptr); + celix_ei_expect_realloc(nullptr, 0, nullptr); + celix_ei_expect_calloc(nullptr, 0, nullptr); + celix_ei_expect_strdup(nullptr, 0, nullptr); + celix_ei_expect_vsnprintf(nullptr, 0, 0); + celix_ei_expect_json_object(nullptr, 0, nullptr); + celix_ei_expect_json_deep_copy(nullptr, 0, nullptr); + celix_ei_expect_json_object_set_new(nullptr, 0, 0); + celix_ei_expect_json_array_append_new(nullptr, 0, 0); + celix_ei_expect_json_array_set_new(nullptr, 0, 0); + celix_ei_expect_json_array_insert_new(nullptr, 0, 0); + celix_ei_expect_json_string(nullptr, 0, nullptr); + celix_ei_expect_json_null(nullptr, 0, nullptr); + } +}; + +/* ── celix_util.c ─────────────────────────────────────────────────────── */ + +TEST_F(JanssonExtErrorInjectionTestSuite, UtilStrbufAppendReallocFail) { + //Given a fresh string buffer and realloc is injected to fail in strbuf_append + celix_jansson_strbuf_t sb; + celix_jansson_strbuf_init(&sb); + celix_ei_expect_realloc((void*)celix_jansson_strbuf_append, 0, nullptr); + //Then appending should fail + EXPECT_EQ(-1, celix_jansson_strbuf_append(&sb, "hello", 5)); + celix_jansson_strbuf_free(&sb); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UtilStrbufVappendfReallocFail) { + //Given a fresh string buffer and realloc is injected to fail in vappendf + celix_jansson_strbuf_t sb; + celix_jansson_strbuf_init(&sb); + celix_ei_expect_realloc((void*)celix_jansson_strbuf_vappendf, 0, nullptr); + //Then printf-appending should fail + EXPECT_EQ(-1, celix_jansson_strbuf_appendf(&sb, "%s", "hello")); + celix_jansson_strbuf_free(&sb); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UtilStrbufVappendfVsprintfFail) { + //Given a fresh string buffer and vsnprintf (with a plausible error code) + //is injected to fail on the length probe in vappendf + celix_jansson_strbuf_t sb; + celix_jansson_strbuf_init(&sb); + celix_ei_expect_vsnprintf((void*)celix_jansson_strbuf_vappendf, 0, -1); + //Then printf-appending should fail without touching the buffer + EXPECT_EQ(-1, celix_jansson_strbuf_appendf(&sb, "%s", "hello")); + EXPECT_EQ(0, sb.len); + celix_jansson_strbuf_free(&sb); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UtilVecPushReallocFail) { + //Given a fresh vec and realloc is injected to fail in vec_push + celix_jansson_vec_t v; + celix_jansson_vec_init(&v); + celix_ei_expect_realloc((void*)celix_jansson_vec_push, 0, nullptr); + //Then pushing an item should fail + EXPECT_EQ(-1, celix_jansson_vec_push(&v, (void*)0x1)); + celix_jansson_vec_free(&v); +} + +/* ── celix_jansson_pointer.c ──────────────────────────────────────────── */ + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerEnsureCapReallocFail) { + //Given realloc is injected to fail in ensure_cap (static, called from push) + celix_ei_expect_realloc((void*)celix_json_pointer_push, 1, nullptr); + celix_json_pointer_t p{}; + //Then pushing a token should fail + EXPECT_EQ(-1, celix_json_pointer_push(&p, "a")); + celix_json_pointer_clear(&p); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerCreateCallocFail) { + //Given calloc is injected to fail in create + celix_ei_expect_calloc((void*)celix_json_pointer_create, 0, nullptr); + //Then creating a pointer should fail + EXPECT_EQ(nullptr, celix_json_pointer_create("/a")); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerInitDecodeMallocFail) { + //Given malloc is injected to fail for the token decode in init + celix_ei_expect_malloc((void*)celix_json_pointer_init, 0, nullptr); + celix_json_pointer_t p{}; + //Then initializing a pointer should fail + EXPECT_EQ(-1, celix_json_pointer_init(&p, "/a")); + celix_json_pointer_clear(&p); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerInitPushStrdupFail) { + //Given strdup is injected to fail in push while init pushes the first token + celix_ei_expect_strdup((void*)celix_json_pointer_push, 0, nullptr); + celix_json_pointer_t p{}; + //Then initializing a pointer should fail + EXPECT_EQ(-1, celix_json_pointer_init(&p, "/a/b")); + celix_json_pointer_clear(&p); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerInitTrailingSlashPushFail) { + //Given strdup is injected to fail only on the second push (the empty trailing token) + celix_ei_expect_strdup((void*)celix_json_pointer_push, 0, nullptr, 2); + celix_json_pointer_t p{}; + //Then initializing "/a/" should fail + EXPECT_EQ(-1, celix_json_pointer_init(&p, "/a/")); + celix_json_pointer_clear(&p); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerCopyCallocFail) { + //Given a pointer with one token + celix_autoptr(celix_json_pointer_t) src = celix_json_pointer_create("/a"); + ASSERT_NE(nullptr, src); + //And calloc is injected to fail in copy + celix_ei_expect_calloc((void*)celix_json_pointer_copy, 0, nullptr); + //Then copying should fail + EXPECT_EQ(nullptr, celix_json_pointer_copy(src)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerCopyEnsureCapReallocFail) { + //Given a pointer with one token and realloc is injected to fail in ensure_cap (called from copy) + celix_autoptr(celix_json_pointer_t) src = celix_json_pointer_create("/a"); + ASSERT_NE(nullptr, src); + celix_ei_expect_realloc((void*)celix_json_pointer_copy, 1, nullptr); + //Then copying should fail + EXPECT_EQ(nullptr, celix_json_pointer_copy(src)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerCopyStrdupFail) { + //Given a pointer with one token and strdup is injected to fail in copy + celix_autoptr(celix_json_pointer_t) src = celix_json_pointer_create("/a"); + ASSERT_NE(nullptr, src); + celix_ei_expect_strdup((void*)celix_json_pointer_copy, 0, nullptr); + //Then copying should fail + EXPECT_EQ(nullptr, celix_json_pointer_copy(src)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerPushStrdupFail) { + //Given strdup is injected to fail in push + celix_ei_expect_strdup((void*)celix_json_pointer_push, 0, nullptr); + celix_json_pointer_t p{}; + //Then pushing a token should fail + EXPECT_EQ(-1, celix_json_pointer_push(&p, "abc")); + celix_json_pointer_clear(&p); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerToStringMallocFail) { + //Given a pointer with one token and malloc is injected to fail in to_string + celix_autoptr(celix_json_pointer_t) src = celix_json_pointer_create("/a"); + ASSERT_NE(nullptr, src); + celix_ei_expect_malloc((void*)celix_json_pointer_to_string, 0, nullptr); + //Then serializing should fail + EXPECT_EQ(nullptr, celix_json_pointer_to_string(src)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerEscapeMallocFail) { + //Given malloc is injected to fail in escape + celix_ei_expect_malloc((void*)celix_json_pointer_escape, 0, nullptr); + //Then escaping should fail + EXPECT_EQ(nullptr, celix_json_pointer_escape("a/b")); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerUnescapeMallocFail) { + //Given malloc is injected to fail in unescape + celix_ei_expect_malloc((void*)celix_json_pointer_unescape, 0, nullptr); + //Then unescaping should fail + EXPECT_EQ(nullptr, celix_json_pointer_unescape("a~1b")); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerParentCallocFail) { + //Given a two-token pointer and calloc is injected to fail in parent (out == nullptr, self-allocated) + celix_autoptr(celix_json_pointer_t) src = celix_json_pointer_create("/a/b"); + ASSERT_NE(nullptr, src); + celix_ei_expect_calloc((void*)celix_json_pointer_parent, 0, nullptr); + //Then computing the parent should fail + EXPECT_EQ(nullptr, celix_json_pointer_parent(src, nullptr)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerParentEnsureCapFail) { + //Given a two-token pointer and realloc is injected to fail in ensure_cap (called from parent) + celix_autoptr(celix_json_pointer_t) src = celix_json_pointer_create("/a/b"); + ASSERT_NE(nullptr, src); + celix_ei_expect_realloc((void*)celix_json_pointer_parent, 1, nullptr); + //Then computing the parent should fail + EXPECT_EQ(nullptr, celix_json_pointer_parent(src, nullptr)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerParentStrdupFail) { + //Given a two-token pointer and strdup is injected to fail in parent + celix_autoptr(celix_json_pointer_t) src = celix_json_pointer_create("/a/b"); + ASSERT_NE(nullptr, src); + celix_ei_expect_strdup((void*)celix_json_pointer_parent, 0, nullptr); + //Then computing the parent should fail + EXPECT_EQ(nullptr, celix_json_pointer_parent(src, nullptr)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerConcatPushFail) { + //Given strdup is injected to fail in push (used by concat) + celix_autoptr(celix_json_pointer_t) dst = celix_json_pointer_create(""); + celix_autoptr(celix_json_pointer_t) suffix = celix_json_pointer_create("/x"); + ASSERT_NE(nullptr, dst); + ASSERT_NE(nullptr, suffix); + celix_ei_expect_strdup((void*)celix_json_pointer_push, 0, nullptr); + //Then concatenating should fail + EXPECT_EQ(-1, celix_json_pointer_concat(dst, suffix)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerInitRootEmptyPushFail) { + //Given strdup is injected to fail in push while init parses the root-empty pointer "/" + celix_ei_expect_strdup((void*)celix_json_pointer_push, 0, nullptr); + celix_json_pointer_t p{}; + //Then initializing should fail and leave the pointer in the cleared state + EXPECT_EQ(-1, celix_json_pointer_init(&p, "/")); + EXPECT_EQ(nullptr, p.tokens); + EXPECT_EQ(0u, celix_json_pointer_depth(&p)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerCreateCleanupOnInitFail) { + //Given strdup is injected to fail on the second push while create parses "/a/b" + celix_ei_expect_strdup((void*)celix_json_pointer_push, 0, nullptr, 2); + //Then creating should fail without leaking the first token or the tokens buffer + EXPECT_EQ(nullptr, celix_json_pointer_create("/a/b")); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerGetOrCreateNullFail) { + //Given json_null is injected to fail for the final node in get_or_create + json_auto_t* doc = json_object(); + celix_autoptr(celix_json_pointer_t) p = celix_json_pointer_create("/a"); + ASSERT_NE(nullptr, doc); + ASSERT_NE(nullptr, p); + celix_ei_expect_json_null((void*)celix_json_pointer_get_or_create, 0, nullptr); + //Then get_or_create should fail and not insert the node + EXPECT_EQ(nullptr, celix_json_pointer_get_or_create(doc, p)); + EXPECT_EQ(nullptr, json_object_get(doc, "a")); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerGetOrCreateSetFail) { + //Given json_object_set_new is injected to fail in the final write of get_or_create. + //json_object_set is a static inline calling set_new; level 1 resolves to get_or_create. + json_auto_t* doc = json_object(); + celix_autoptr(celix_json_pointer_t) p = celix_json_pointer_create("/a"); + ASSERT_NE(nullptr, doc); + ASSERT_NE(nullptr, p); + celix_ei_expect_json_object_set_new((void*)celix_json_pointer_get_or_create, 1, -1); + //Then get_or_create should fail and not insert the node + EXPECT_EQ(nullptr, celix_json_pointer_get_or_create(doc, p)); + EXPECT_EQ(nullptr, json_object_get(doc, "a")); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerGetOrCreateIntermediateSetNewFail) { + //Given json_object_set_new is injected to fail while inserting the intermediate object + json_auto_t* doc = json_object(); + celix_autoptr(celix_json_pointer_t) p = celix_json_pointer_create("/a/b"); + ASSERT_NE(nullptr, doc); + ASSERT_NE(nullptr, p); + celix_ei_expect_json_object_set_new((void*)celix_json_pointer_get_or_create, 0, -1); + //Then get_or_create should fail and not insert the intermediate node + EXPECT_EQ(nullptr, celix_json_pointer_get_or_create(doc, p)); + EXPECT_EQ(nullptr, json_object_get(doc, "a")); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerGetOrCreateIntermediateJsonObjectFail) { + //Given json_object is injected to fail for the intermediate container in get_or_create + json_auto_t* doc = json_object(); + celix_autoptr(celix_json_pointer_t) p = celix_json_pointer_create("/a/b"); + ASSERT_NE(nullptr, doc); + ASSERT_NE(nullptr, p); + celix_ei_expect_json_object((void*)celix_json_pointer_get_or_create, 0, nullptr); + //Then get_or_create should fail and not insert the intermediate node + EXPECT_EQ(nullptr, celix_json_pointer_get_or_create(doc, p)); + EXPECT_EQ(nullptr, json_object_get(doc, "a")); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerGetOrCreatePaddingNullFail) { + //Given json_null is injected to fail while padding the target array in get_or_create + json_auto_t* doc = json_array(); + celix_autoptr(celix_json_pointer_t) p = celix_json_pointer_create("/2"); + ASSERT_NE(nullptr, doc); + ASSERT_NE(nullptr, p); + celix_ei_expect_json_null((void*)celix_json_pointer_get_or_create, 0, nullptr); + //Then get_or_create should fail and the array must remain empty + EXPECT_EQ(nullptr, celix_json_pointer_get_or_create(doc, p)); + EXPECT_EQ(0u, json_array_size(doc)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerGetOrCreatePaddingAppendFail) { + //Given json_array_append_new is injected to fail while padding the target array in get_or_create + json_auto_t* doc = json_array(); + celix_autoptr(celix_json_pointer_t) p = celix_json_pointer_create("/2"); + ASSERT_NE(nullptr, doc); + ASSERT_NE(nullptr, p); + celix_ei_expect_json_array_append_new((void*)celix_json_pointer_get_or_create, 0, -1); + //Then get_or_create should fail and the array must remain empty + EXPECT_EQ(nullptr, celix_json_pointer_get_or_create(doc, p)); + EXPECT_EQ(0u, json_array_size(doc)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerGetOrCreateDashAppendFail) { + //Given json_array_append_new is injected to fail while appending the "-" element in get_or_create + json_auto_t* doc = json_array(); + celix_autoptr(celix_json_pointer_t) p = celix_json_pointer_create("/-"); + ASSERT_NE(nullptr, doc); + ASSERT_NE(nullptr, p); + celix_ei_expect_json_array_append_new((void*)celix_json_pointer_get_or_create, 0, -1); + //Then get_or_create should fail and the array must remain empty + EXPECT_EQ(nullptr, celix_json_pointer_get_or_create(doc, p)); + EXPECT_EQ(0u, json_array_size(doc)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerGetOrCreateDashNullFail) { + //Given json_null is injected to fail while appending the "-" element in get_or_create + json_auto_t* doc = json_array(); + celix_autoptr(celix_json_pointer_t) p = celix_json_pointer_create("/-"); + ASSERT_NE(nullptr, doc); + ASSERT_NE(nullptr, p); + celix_ei_expect_json_null((void*)celix_json_pointer_get_or_create, 0, nullptr); + //Then get_or_create should fail and the array must remain empty + EXPECT_EQ(nullptr, celix_json_pointer_get_or_create(doc, p)); + EXPECT_EQ(0u, json_array_size(doc)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerSetFinalSetNewFail) { + //Given json_object_set_new is injected to fail in the final write of set + json_auto_t* doc = json_object(); + celix_autoptr(celix_json_pointer_t) p = celix_json_pointer_create("/a"); + ASSERT_NE(nullptr, doc); + ASSERT_NE(nullptr, p); + celix_ei_expect_json_object_set_new((void*)celix_json_pointer_set, 0, -1); + //Then setting should fail; the value is consumed by the failed set_new + EXPECT_EQ(-1, celix_json_pointer_set(doc, p, json_integer(42))); + EXPECT_EQ(nullptr, json_object_get(doc, "a")); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerSetFinalAppendNewFail) { + //Given json_array_append_new is injected to fail in the final "-" write of set + json_auto_t* doc = json_loads(R"({"a":[1,2]})", 0, nullptr); + celix_autoptr(celix_json_pointer_t) p = celix_json_pointer_create("/a/-"); + ASSERT_NE(nullptr, doc); + ASSERT_NE(nullptr, p); + celix_ei_expect_json_array_append_new((void*)celix_json_pointer_set, 0, -1); + //Then setting should fail and the document must remain unchanged + EXPECT_EQ(-1, celix_json_pointer_set(doc, p, json_integer(42))); + char* dumped = json_dumps(doc, JSON_COMPACT); + ASSERT_NE(nullptr, dumped); + EXPECT_STREQ(R"({"a":[1,2]})", dumped); + free(dumped); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerSetIntermediateSetNewFail) { + //Given json_object_set_new is injected to fail while inserting the intermediate object in set + json_auto_t* doc = json_object(); + celix_autoptr(celix_json_pointer_t) p = celix_json_pointer_create("/a/b"); + ASSERT_NE(nullptr, doc); + ASSERT_NE(nullptr, p); + celix_ei_expect_json_object_set_new((void*)celix_json_pointer_set, 0, -1); + //Then setting should fail and the intermediate node must not be inserted + EXPECT_EQ(-1, celix_json_pointer_set(doc, p, json_integer(42))); + EXPECT_EQ(nullptr, json_object_get(doc, "a")); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerSetIntermediateJsonArrayFail) { + //Given json_array is injected to fail for the intermediate container in set + json_auto_t* doc = json_object(); + celix_autoptr(celix_json_pointer_t) p = celix_json_pointer_create("/a/0/x"); + ASSERT_NE(nullptr, doc); + ASSERT_NE(nullptr, p); + celix_ei_expect_json_array((void*)celix_json_pointer_set, 0, nullptr); + //Then setting should fail and the intermediate node must not be inserted + EXPECT_EQ(-1, celix_json_pointer_set(doc, p, json_integer(42))); + EXPECT_EQ(nullptr, json_object_get(doc, "a")); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerSetIntermediateReplFail) { + //Given json_object is injected to fail for the replacement container in set + json_auto_t* doc = json_loads("[1]", 0, nullptr); + celix_autoptr(celix_json_pointer_t) p = celix_json_pointer_create("/0/b"); + ASSERT_NE(nullptr, doc); + ASSERT_NE(nullptr, p); + celix_ei_expect_json_object((void*)celix_json_pointer_set, 0, nullptr); + //Then setting should fail and the document must remain unchanged + EXPECT_EQ(-1, celix_json_pointer_set(doc, p, json_integer(42))); + EXPECT_EQ(1u, json_array_size(doc)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerSetPaddingAppendFail) { + //Given json_array_append_new is injected to fail while padding the target array in set + json_auto_t* doc = json_loads(R"({"arr":[]})", 0, nullptr); + celix_autoptr(celix_json_pointer_t) p = celix_json_pointer_create("/arr/2"); + ASSERT_NE(nullptr, doc); + ASSERT_NE(nullptr, p); + celix_ei_expect_json_array_append_new((void*)celix_json_pointer_set, 0, -1); + //Then setting should fail and the array must remain empty + EXPECT_EQ(-1, celix_json_pointer_set(doc, p, json_integer(42))); + EXPECT_EQ(0u, json_array_size(json_object_get(doc, "arr"))); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerSetPaddingNullFail) { + //Given json_null is injected to fail while padding the target array in set + json_auto_t* doc = json_loads(R"({"arr":[]})", 0, nullptr); + celix_autoptr(celix_json_pointer_t) p = celix_json_pointer_create("/arr/2"); + ASSERT_NE(nullptr, doc); + ASSERT_NE(nullptr, p); + celix_ei_expect_json_null((void*)celix_json_pointer_set, 0, nullptr); + //Then setting should fail and the array must remain empty + EXPECT_EQ(-1, celix_json_pointer_set(doc, p, json_integer(42))); + EXPECT_EQ(0u, json_array_size(json_object_get(doc, "arr"))); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerSetIntermediatePaddingAppendFail) { + //Given json_array_append_new is injected to fail while padding an intermediate array in set. + //"/arr/2/0" makes the padding happen before the walk descends into the new array slot. + json_auto_t* doc = json_loads(R"({"arr":[]})", 0, nullptr); + celix_autoptr(celix_json_pointer_t) p = celix_json_pointer_create("/arr/2/0"); + ASSERT_NE(nullptr, doc); + ASSERT_NE(nullptr, p); + celix_ei_expect_json_array_append_new((void*)celix_json_pointer_set, 0, -1); + //Then setting should fail and the array must remain empty + EXPECT_EQ(-1, celix_json_pointer_set(doc, p, json_integer(42))); + EXPECT_EQ(0u, json_array_size(json_object_get(doc, "arr"))); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PointerRemovePushFail) { + //Given strdup is injected to fail while building the parent pointer in remove. + //The root also has a key "c", so the OLD code would resolve the parent to the + //root and delete the wrong node. + json_auto_t* doc = json_loads(R"({"a":{"c":1},"c":99})", 0, nullptr); + celix_autoptr(celix_json_pointer_t) p = celix_json_pointer_create("/a/c"); + ASSERT_NE(nullptr, doc); + ASSERT_NE(nullptr, p); + celix_ei_expect_strdup((void*)celix_json_pointer_push, 0, nullptr); + //Then removing should fail and the document must remain unchanged + EXPECT_EQ(-1, celix_json_pointer_remove(doc, p)); + char* dumped = json_dumps(doc, JSON_COMPACT); + ASSERT_NE(nullptr, dumped); + EXPECT_STREQ(R"({"a":{"c":1},"c":99})", dumped); + free(dumped); +} + +/* ── celix_jansson_uri.c ──────────────────────────────────────────────── */ + +TEST_F(JanssonExtErrorInjectionTestSuite, UriUpdateLocationMallocFail) { + //Given malloc is injected to fail for the location in update + celix_jansson_uri_t u{}; + celix_ei_expect_malloc((void*)celix_jansson_uri_update, 0, nullptr); + //Then updating the URI should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_update(&u, "http://example.com")); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriUpdatePercentDecodeReallocFail) { + //Given realloc is injected to fail in strbuf_append (used by percent_decode). + //The single-char fragment makes percent_decode loop exactly once, so the + //single injected realloc failure is not recovered by a later iteration. + celix_jansson_uri_t u{}; + celix_ei_expect_realloc((void*)celix_jansson_strbuf_append, 0, nullptr); + //Then updating a URI with a fragment should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_update(&u, "http://example.com#a")); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriUpdateInvalidPointerFragment) { + //Given a fragment that is not a valid JSON Pointer ("~" without escape character) + celix_jansson_uri_t u{}; + //Then updating should fail with NOMEM (pointer init failure is mapped to NOMEM) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_update(&u, "http://example.com#/a~")); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriAppendCopyPushFail) { + //Given a URI with one pointer token and strdup is injected to fail in push while copying tokens + celix_jansson_uri_t u{}; + ASSERT_EQ(0, celix_jansson_uri_init(&u, "#/a")); + celix_jansson_uri_t out{}; + celix_ei_expect_strdup((void*)celix_json_pointer_push, 0, nullptr); + //Then appending a token should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_append(&u, "x", &out)); + celix_jansson_uri_clear(&u); + celix_jansson_uri_clear(&out); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriAppendFinalPushFail) { + //Given a URI without pointer tokens and strdup is injected to fail in push for the final token + celix_jansson_uri_t u{}; + ASSERT_EQ(0, celix_jansson_uri_init(&u, "http://example.com")); + celix_jansson_uri_t out{}; + celix_ei_expect_strdup((void*)celix_json_pointer_push, 0, nullptr); + //Then appending a token should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_append(&u, "x", &out)); + celix_jansson_uri_clear(&u); + celix_jansson_uri_clear(&out); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriUpdateSchemeMallocFail) { + //Given malloc is injected to fail for the scheme (update's 2nd malloc: + //#1 is the location buffer; the authority is strdup'd, not malloc'd) + celix_jansson_uri_t u{}; + celix_ei_expect_malloc((void*)celix_jansson_uri_update, 0, nullptr, 2); + //Then updating the URI should fail with NOMEM instead of silently dropping the scheme + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_update(&u, "http://example.com")); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriUpdateAuthorityStrdupFail) { + //Given strdup is injected to fail for the authority ("http://example.com" has + //no slash, so its only strdup is the authority) + celix_jansson_uri_t u{}; + celix_ei_expect_strdup((void*)celix_jansson_uri_update, 0, nullptr); + //Then updating the URI should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_update(&u, "http://example.com")); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriUpdatePathStrdupFail) { + //Given strdup is injected to fail for the path ("http://example.com/path" has + //a slash, so its first strdup is the path) + celix_jansson_uri_t u{}; + celix_ei_expect_strdup((void*)celix_jansson_uri_update, 0, nullptr); + //Then updating the URI should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_update(&u, "http://example.com/path")); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriUpdateOldPathStrdupFail) { + //Given strdup is injected to fail for the old-path copy during relative resolution + celix_jansson_uri_t u{}; + ASSERT_EQ(0, celix_jansson_uri_init(&u, "http://example.com/a")); + celix_ei_expect_strdup((void*)celix_jansson_uri_update, 0, nullptr); + //Then updating should fail with NOMEM before u is modified + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_update(&u, "b")); + ASSERT_NE(nullptr, u.path); + EXPECT_STREQ("/a", u.path); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriDeriveCopyStrdupFail) { + //Given strdup is injected to fail while copying the base components in derive + celix_jansson_uri_t base{}; + ASSERT_EQ(0, celix_jansson_uri_init(&base, "http://example.com/a")); + celix_jansson_uri_t out{}; + celix_ei_expect_strdup((void*)celix_jansson_uri_derive, 0, nullptr); + //Then deriving should fail with NOMEM (out is left cleared) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_derive(&base, "x", &out)); + celix_jansson_uri_clear(&base); + celix_jansson_uri_clear(&out); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriDerivePointerPushFail) { + //Given strdup is injected to fail in push while copying the base pointer tokens. + //The base copy produces no strdup for "#/a" (no location components), so the + //first push strdup fails — today derive silently succeeds with a dropped token. + celix_jansson_uri_t base{}; + ASSERT_EQ(0, celix_jansson_uri_init(&base, "#/a")); + celix_jansson_uri_t out{}; + celix_ei_expect_strdup((void*)celix_json_pointer_push, 0, nullptr); + //Then deriving should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_derive(&base, "#", &out)); + celix_jansson_uri_clear(&base); + celix_jansson_uri_clear(&out); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriAppendIdentifierStrdupFail) { + //Given strdup is injected to fail for the identifier copy in append. + //Today append returns 0 unconditionally on this path. + celix_jansson_uri_t u{}; + ASSERT_EQ(0, celix_jansson_uri_init(&u, "#abc")); + celix_jansson_uri_t out{}; + celix_ei_expect_strdup((void*)celix_jansson_uri_append, 0, nullptr); + //Then appending should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_append(&u, "x", &out)); + celix_jansson_uri_clear(&u); + celix_jansson_uri_clear(&out); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriLocationUrnStrdupFail) { + //Given strdup is injected to fail in location for the URN copy + celix_jansson_uri_t u{}; + ASSERT_EQ(0, celix_jansson_uri_init(&u, "urn:foo")); + celix_ei_expect_strdup((void*)celix_jansson_uri_location, 0, nullptr); + //Then location returns NULL (OOM signal) + EXPECT_EQ(nullptr, celix_jansson_uri_location(&u)); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriToStringNoCrashOnLocationOom) { + //Given strdup is injected to fail in location while to_string runs + //(location returns NULL; today to_string crashes with strlen(NULL)) + celix_jansson_uri_t u{}; + ASSERT_EQ(0, celix_jansson_uri_init(&u, "urn:foo")); + celix_ei_expect_strdup((void*)celix_jansson_uri_location, 0, nullptr); + //Then to_string must not crash and returns NULL (OOM signal) + EXPECT_EQ(nullptr, celix_jansson_uri_to_string(&u)); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriEscapeEmpty) { + //Given an empty string (no injection) + //Then escape("") must return "" rather than NULL + char* r = celix_jansson_uri_escape(""); + ASSERT_NE(nullptr, r); + EXPECT_STREQ("", r); + free(r); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriEscapeAppendFail) { + //Given realloc is injected to fail in strbuf_append while escaping. + //"a~" appends exactly once before the failure, so the one-shot injection + //is not recovered by a later iteration. + celix_ei_expect_realloc((void*)celix_jansson_strbuf_append, 0, nullptr); + //Then escape must return NULL (OOM signal), not a partial string + EXPECT_EQ(nullptr, celix_jansson_uri_escape("a~")); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriEqualsOomNoCrash) { + //Given strdup is injected to fail in location for the first URI + celix_jansson_uri_t a{}; + celix_jansson_uri_t b{}; + ASSERT_EQ(0, celix_jansson_uri_init(&a, "urn:foo")); + ASSERT_EQ(0, celix_jansson_uri_init(&b, "urn:bar")); + celix_ei_expect_strdup((void*)celix_jansson_uri_location, 0, nullptr); + //Then equals degrades to false instead of strcmp(NULL, ...) crashing + EXPECT_FALSE(celix_jansson_uri_equals(&a, &b)); + celix_jansson_uri_clear(&a); + celix_jansson_uri_clear(&b); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriInitFailureLeavesCleared) { + //Given malloc is injected to fail for the location buffer in update + celix_jansson_uri_t u{}; + celix_ei_expect_malloc((void*)celix_jansson_uri_update, 0, nullptr); + //Then init fails with NOMEM and u is left fully cleared + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_init(&u, "http://example.com")); + EXPECT_EQ(nullptr, u.scheme); + EXPECT_EQ(nullptr, u.authority); + EXPECT_EQ(nullptr, u.path); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriUpdateOldSchemeStrdupFail) { + //Given strdup is injected to fail for the old-scheme copy (2nd strdup in update) + celix_jansson_uri_t u{}; + ASSERT_EQ(0, celix_jansson_uri_init(&u, "http://example.com/a")); + celix_ei_expect_strdup((void*)celix_jansson_uri_update, 0, nullptr, 2); + //Then updating should fail with NOMEM before u is modified + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_update(&u, "b")); + ASSERT_NE(nullptr, u.scheme); + EXPECT_STREQ("http", u.scheme); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriUpdateOldAuthorityStrdupFail) { + //Given strdup is injected to fail for the old-authority copy (3rd strdup in update) + celix_jansson_uri_t u{}; + ASSERT_EQ(0, celix_jansson_uri_init(&u, "http://example.com/a")); + celix_ei_expect_strdup((void*)celix_jansson_uri_update, 0, nullptr, 3); + //Then updating should fail with NOMEM before u is modified + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_update(&u, "b")); + ASSERT_NE(nullptr, u.authority); + EXPECT_STREQ("example.com", u.authority); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriUpdateAuthorityMallocFail) { + //Given malloc is injected to fail for the authority (update's 3rd malloc: + //#1 location, #2 scheme, #3 authority — the path is strdup'd) + celix_jansson_uri_t u{}; + celix_ei_expect_malloc((void*)celix_jansson_uri_update, 0, nullptr, 3); + //Then updating the URI should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_update(&u, "http://example.com/path")); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriUpdateAbsolutePathStrdupFail) { + //Given strdup is injected to fail for the absolute-path copy + //(fresh u has no old components, so the first strdup is the path) + celix_jansson_uri_t u{}; + celix_ei_expect_strdup((void*)celix_jansson_uri_update, 0, nullptr); + //Then updating with an absolute path should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_update(&u, "/x")); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriUpdateRelativeStrbufFail) { + //Given realloc is injected to fail in strbuf_append during relative-path + //resolution (the only strbuf_append call in this update) + celix_jansson_uri_t u{}; + ASSERT_EQ(0, celix_jansson_uri_init(&u, "http://example.com/a/b")); + celix_ei_expect_realloc((void*)celix_jansson_strbuf_append, 0, nullptr); + //Then updating should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_update(&u, "x")); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriUpdateRelativeNoDirStrdupFail) { + //Given a URI whose path has no directory (no '/') and strdup is injected + //to fail for the relative-path copy (2nd strdup: #1 is the old-path copy) + celix_jansson_uri_t u{}; + u.path = strdup("abc"); + ASSERT_NE(nullptr, u.path); + celix_ei_expect_strdup((void*)celix_jansson_uri_update, 0, nullptr, 2); + //Then updating should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_update(&u, "x")); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriUpdateRelativeNoBaseStrdupFail) { + //Given a fresh URI (no base path) and strdup is injected to fail for the + //relative-path copy (the first strdup in update) + celix_jansson_uri_t u{}; + celix_ei_expect_strdup((void*)celix_jansson_uri_update, 0, nullptr); + //Then updating should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_update(&u, "x")); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriDeriveAuthorityStrdupFail) { + //Given strdup is injected to fail for the authority copy (2nd strdup in derive) + celix_jansson_uri_t base{}; + ASSERT_EQ(0, celix_jansson_uri_init(&base, "http://example.com/a")); + celix_jansson_uri_t out{}; + celix_ei_expect_strdup((void*)celix_jansson_uri_derive, 0, nullptr, 2); + //Then deriving should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_derive(&base, "x", &out)); + celix_jansson_uri_clear(&base); + celix_jansson_uri_clear(&out); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriDerivePathStrdupFail) { + //Given strdup is injected to fail for the path copy (3rd strdup in derive) + celix_jansson_uri_t base{}; + ASSERT_EQ(0, celix_jansson_uri_init(&base, "http://example.com/a")); + celix_jansson_uri_t out{}; + celix_ei_expect_strdup((void*)celix_jansson_uri_derive, 0, nullptr, 3); + //Then deriving should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_derive(&base, "x", &out)); + celix_jansson_uri_clear(&base); + celix_jansson_uri_clear(&out); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriDeriveUrnStrdupFail) { + //Given a base carrying all four components and strdup is injected to fail + //for the urn copy (4th strdup in derive) + celix_jansson_uri_t base{}; + base.scheme = strdup("http"); + base.authority = strdup("example.com"); + base.path = strdup("/a"); + base.urn = strdup("urn:foo"); + celix_jansson_uri_t out{}; + celix_ei_expect_strdup((void*)celix_jansson_uri_derive, 0, nullptr, 4); + //Then deriving should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_derive(&base, "x", &out)); + celix_jansson_uri_clear(&base); + celix_jansson_uri_clear(&out); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriDeriveIdentifierStrdupFail) { + //Given a URI with an identifier fragment and strdup is injected to fail + //while copying it in derive (no location components to copy) + celix_jansson_uri_t u{}; + ASSERT_EQ(0, celix_jansson_uri_init(&u, "#abc")); + celix_jansson_uri_t out{}; + celix_ei_expect_strdup((void*)celix_jansson_uri_derive, 0, nullptr); + //Then deriving should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_derive(&u, "x", &out)); + celix_jansson_uri_clear(&u); + celix_jansson_uri_clear(&out); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriAppendComponentStrdupFail) { + //Given strdup is injected to fail while copying the scheme component in append + celix_jansson_uri_t u{}; + ASSERT_EQ(0, celix_jansson_uri_init(&u, "http://example.com/a")); + celix_jansson_uri_t out{}; + celix_ei_expect_strdup((void*)celix_jansson_uri_append, 0, nullptr); + //Then appending a token should fail with NOMEM (out is left cleared) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_append(&u, "x", &out)); + celix_jansson_uri_clear(&u); + celix_jansson_uri_clear(&out); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriAppendAuthorityStrdupFail) { + //Given strdup is injected to fail while copying the authority component (2nd) + celix_jansson_uri_t u{}; + ASSERT_EQ(0, celix_jansson_uri_init(&u, "http://example.com/a")); + celix_jansson_uri_t out{}; + celix_ei_expect_strdup((void*)celix_jansson_uri_append, 0, nullptr, 2); + //Then appending a token should fail with NOMEM (out is left cleared) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_append(&u, "x", &out)); + celix_jansson_uri_clear(&u); + celix_jansson_uri_clear(&out); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriAppendPathStrdupFail) { + //Given strdup is injected to fail while copying the path component (3rd) + celix_jansson_uri_t u{}; + ASSERT_EQ(0, celix_jansson_uri_init(&u, "http://example.com/a")); + celix_jansson_uri_t out{}; + celix_ei_expect_strdup((void*)celix_jansson_uri_append, 0, nullptr, 3); + //Then appending a token should fail with NOMEM (out is left cleared) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_append(&u, "x", &out)); + celix_jansson_uri_clear(&u); + celix_jansson_uri_clear(&out); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriAppendUrnStrdupFail) { + //Given a URI carrying all four components and strdup is injected to fail + //while copying the urn component (4th) + celix_jansson_uri_t u{}; + u.scheme = strdup("http"); + u.authority = strdup("example.com"); + u.path = strdup("/a"); + u.urn = strdup("urn:foo"); + celix_jansson_uri_t out{}; + celix_ei_expect_strdup((void*)celix_jansson_uri_append, 0, nullptr, 4); + //Then appending a token should fail with NOMEM (out is left cleared) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_uri_append(&u, "x", &out)); + celix_jansson_uri_clear(&u); + celix_jansson_uri_clear(&out); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriLocationSchemeAppendsFail) { + //Given realloc is injected to fail in strbuf_append for the first append + //(the scheme) inside location + celix_jansson_uri_t u{}; + ASSERT_EQ(0, celix_jansson_uri_init(&u, "http://example.com/a")); + celix_ei_expect_realloc((void*)celix_jansson_strbuf_append, 0, nullptr); + //Then location returns NULL (OOM signal) + EXPECT_EQ(nullptr, celix_jansson_uri_location(&u)); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriLocationAuthorityAppendsFail) { + //Given a long authority (scheme "://" fit the initial 64-byte buffer) and + //realloc is injected to fail for the 2nd realloc: #1 scheme appends, + //#2 authority appends (78 bytes needed > 64) + celix_jansson_uri_t u{}; + ASSERT_EQ(0, celix_jansson_uri_init(&u, "http://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); + celix_ei_expect_realloc((void*)celix_jansson_strbuf_append, 0, nullptr, 2); + //Then location returns NULL (OOM signal) + EXPECT_EQ(nullptr, celix_jansson_uri_location(&u)); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriLocationPathAppendsFail) { + //Given a long path (scheme "://" authority fit the initial 64-byte buffer) + //and realloc is injected to fail for the 2nd realloc: #1 scheme appends, + //#2 path appends (79 bytes needed > 64) + celix_jansson_uri_t u{}; + ASSERT_EQ(0, celix_jansson_uri_init(&u, "http://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")); + celix_ei_expect_realloc((void*)celix_jansson_strbuf_append, 0, nullptr, 2); + //Then location returns NULL (OOM signal) + EXPECT_EQ(nullptr, celix_jansson_uri_location(&u)); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriToStringAppendsFail) { + //Given a URN (location returns via strdup, no strbuf) and realloc is + //injected to fail for to_string's own first append of the location part + celix_jansson_uri_t u{}; + ASSERT_EQ(0, celix_jansson_uri_init(&u, "urn:foo")); + celix_ei_expect_realloc((void*)celix_jansson_strbuf_append, 0, nullptr); + //Then to_string returns NULL (OOM signal) + EXPECT_EQ(nullptr, celix_jansson_uri_to_string(&u)); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriToStringFragmentAppendsFail) { + //Given a URN with a long fragment (location "#" fit the 64-byte buffer, + //the fragment appends needs 132 bytes > 64) and realloc is injected to + //fail for the 2nd realloc: #1 location appends, #2 fragment appends + celix_jansson_uri_t u{}; + ASSERT_EQ(0, celix_jansson_uri_init(&u, "urn:aaaaaaaaaaaaaaaaaaaaaaaaaa#bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")); + celix_ei_expect_realloc((void*)celix_jansson_strbuf_append, 0, nullptr, 2); + //Then to_string returns NULL (OOM signal) + EXPECT_EQ(nullptr, celix_jansson_uri_to_string(&u)); + celix_jansson_uri_clear(&u); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, UriEqualsFragmentOomNoCrash) { + //Given equal locations and strdup is injected to fail in fragment for the + //first URI — equals must degrade to false instead of strcmp(NULL, ...) + celix_jansson_uri_t a{}; + celix_jansson_uri_t b{}; + ASSERT_EQ(0, celix_jansson_uri_init(&a, "urn:foo")); + ASSERT_EQ(0, celix_jansson_uri_init(&b, "urn:foo")); + celix_ei_expect_strdup((void*)celix_jansson_uri_fragment, 0, nullptr); + //Then equals returns false without crashing + EXPECT_FALSE(celix_jansson_uri_equals(&a, &b)); + celix_jansson_uri_clear(&a); + celix_jansson_uri_clear(&b); +} + +/* ── celix_json_patch.c ───────────────────────────────────────────────── */ + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchAddJsonObjectFail) { + //Given json_object is injected to fail in patch_add + json_auto_t* patch = json_array(); + celix_ei_expect_json_object((void*)celix_json_patch_add, 0, nullptr); + //Then adding an operation should fail; the value is consumed on failure + //(json_integer(42) is a fresh reference, so LSAN proves the consumption) + EXPECT_EQ(-1, celix_json_patch_add(patch, "/a", json_integer(42))); + EXPECT_EQ(0u, json_array_size(patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchReplaceJsonObjectFail) { + //Given json_object is injected to fail in patch_replace + json_auto_t* patch = json_array(); + celix_ei_expect_json_object((void*)celix_json_patch_replace, 0, nullptr); + //Then replacing should fail; the value is consumed on failure + EXPECT_EQ(-1, celix_json_patch_replace(patch, "/a", json_integer(42))); + EXPECT_EQ(0u, json_array_size(patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchRemoveJsonObjectFail) { + //Given json_object is injected to fail in patch_remove + json_auto_t* patch = json_array(); + celix_ei_expect_json_object((void*)celix_json_patch_remove, 0, nullptr); + //Then removing should fail + EXPECT_EQ(-1, celix_json_patch_remove(patch, "/a")); + EXPECT_EQ(0u, json_array_size(patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchApplyDeepCopyFail) { + //Given json_deep_copy is injected to fail in patch_apply + json_auto_t* original = json_object(); + json_auto_t* patch = json_array(); + celix_ei_expect_json_deep_copy((void*)celix_json_patch_apply, 0, nullptr); + //Then applying the patch should fail + EXPECT_EQ(nullptr, celix_json_patch_apply(original, patch)); +} + +/* ── builder failures beyond json_object ───────────────────────────────── */ + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchAddOpStringFail) { + //Given the first json_string (the op name) is injected to fail in patch_add. + //json_string is called twice per add: ordinal 1 = op name, ordinal 2 = path. + json_auto_t* patch = json_array(); + celix_ei_expect_json_string((void*)celix_json_patch_add, 0, nullptr); + //Then adding fails and releases both the op object and the value + EXPECT_EQ(-1, celix_json_patch_add(patch, "/a", json_integer(42))); + EXPECT_EQ(0u, json_array_size(patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchAddPathStringFail) { + //Given the second json_string (the path) is injected to fail in patch_add + json_auto_t* patch = json_array(); + celix_ei_expect_json_string((void*)celix_json_patch_add, 0, nullptr, 2); + //Then adding fails and releases both the op object and the value + EXPECT_EQ(-1, celix_json_patch_add(patch, "/a", json_integer(42))); + EXPECT_EQ(0u, json_array_size(patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchAddOpSetNewFail) { + //Given the first json_object_set_new (the "op" key) is injected to fail + //in patch_add; the wrapper releases the op-name string + json_auto_t* patch = json_array(); + celix_ei_expect_json_object_set_new((void*)celix_json_patch_add, 0, -1); + //Then adding fails and releases both the op object and the value + EXPECT_EQ(-1, celix_json_patch_add(patch, "/a", json_integer(42))); + EXPECT_EQ(0u, json_array_size(patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchAddPathSetNewFail) { + //Given the second json_object_set_new (the "path" key) is injected to + //fail in patch_add; the wrapper releases the path string + json_auto_t* patch = json_array(); + celix_ei_expect_json_object_set_new((void*)celix_json_patch_add, 0, -1, 2); + //Then adding fails and releases both the op object and the value + EXPECT_EQ(-1, celix_json_patch_add(patch, "/a", json_integer(42))); + EXPECT_EQ(0u, json_array_size(patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchAddValueSetNewFail) { + //Given the third json_object_set_new (the "value" key) is injected to + //fail in patch_add; the failed set_new consumes the value, so only the + //op object is released here + json_auto_t* patch = json_array(); + celix_ei_expect_json_object_set_new((void*)celix_json_patch_add, 0, -1, 3); + //Then adding fails without appending the op + EXPECT_EQ(-1, celix_json_patch_add(patch, "/a", json_integer(42))); + EXPECT_EQ(0u, json_array_size(patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchAddAppendFail) { + //Given json_array_append_new is injected to fail in patch_add; the + //failed append_new consumes the op, which owns the value + json_auto_t* patch = json_array(); + celix_ei_expect_json_array_append_new((void*)celix_json_patch_add, 0, -1); + //Then adding fails and the patch stays empty + EXPECT_EQ(-1, celix_json_patch_add(patch, "/a", json_integer(42))); + EXPECT_EQ(0u, json_array_size(patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchReplaceOpStringFail) { + //Given the first json_string (the op name) is injected to fail in patch_replace + json_auto_t* patch = json_array(); + celix_ei_expect_json_string((void*)celix_json_patch_replace, 0, nullptr); + //Then replacing fails and releases both the op object and the value + EXPECT_EQ(-1, celix_json_patch_replace(patch, "/a", json_integer(42))); + EXPECT_EQ(0u, json_array_size(patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchReplacePathStringFail) { + //Given the second json_string (the path) is injected to fail in patch_replace + json_auto_t* patch = json_array(); + celix_ei_expect_json_string((void*)celix_json_patch_replace, 0, nullptr, 2); + //Then replacing fails and releases both the op object and the value + EXPECT_EQ(-1, celix_json_patch_replace(patch, "/a", json_integer(42))); + EXPECT_EQ(0u, json_array_size(patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchReplaceOpSetNewFail) { + //Given the first json_object_set_new (the "op" key) is injected to fail + //in patch_replace + json_auto_t* patch = json_array(); + celix_ei_expect_json_object_set_new((void*)celix_json_patch_replace, 0, -1); + //Then replacing fails and releases both the op object and the value + EXPECT_EQ(-1, celix_json_patch_replace(patch, "/a", json_integer(42))); + EXPECT_EQ(0u, json_array_size(patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchReplacePathSetNewFail) { + //Given the second json_object_set_new (the "path" key) is injected to + //fail in patch_replace + json_auto_t* patch = json_array(); + celix_ei_expect_json_object_set_new((void*)celix_json_patch_replace, 0, -1, 2); + //Then replacing fails and releases both the op object and the value + EXPECT_EQ(-1, celix_json_patch_replace(patch, "/a", json_integer(42))); + EXPECT_EQ(0u, json_array_size(patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchReplaceValueSetNewFail) { + //Given the third json_object_set_new (the "value" key) is injected to + //fail in patch_replace; the failed set_new consumes the value + json_auto_t* patch = json_array(); + celix_ei_expect_json_object_set_new((void*)celix_json_patch_replace, 0, -1, 3); + //Then replacing fails without appending the op + EXPECT_EQ(-1, celix_json_patch_replace(patch, "/a", json_integer(42))); + EXPECT_EQ(0u, json_array_size(patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchReplaceAppendFail) { + //Given json_array_append_new is injected to fail in patch_replace; the + //failed append_new consumes the op, which owns the value + json_auto_t* patch = json_array(); + celix_ei_expect_json_array_append_new((void*)celix_json_patch_replace, 0, -1); + //Then replacing fails and the patch stays empty + EXPECT_EQ(-1, celix_json_patch_replace(patch, "/a", json_integer(42))); + EXPECT_EQ(0u, json_array_size(patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchRemoveOpStringFail) { + //Given the first json_string (the op name) is injected to fail in patch_remove + json_auto_t* patch = json_array(); + celix_ei_expect_json_string((void*)celix_json_patch_remove, 0, nullptr); + //Then removing fails and releases the op object + EXPECT_EQ(-1, celix_json_patch_remove(patch, "/a")); + EXPECT_EQ(0u, json_array_size(patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchRemovePathStringFail) { + //Given the second json_string (the path) is injected to fail in patch_remove + json_auto_t* patch = json_array(); + celix_ei_expect_json_string((void*)celix_json_patch_remove, 0, nullptr, 2); + //Then removing fails and releases the op object + EXPECT_EQ(-1, celix_json_patch_remove(patch, "/a")); + EXPECT_EQ(0u, json_array_size(patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchRemoveOpSetNewFail) { + //Given the first json_object_set_new (the "op" key) is injected to fail + //in patch_remove + json_auto_t* patch = json_array(); + celix_ei_expect_json_object_set_new((void*)celix_json_patch_remove, 0, -1); + //Then removing fails and releases the op object + EXPECT_EQ(-1, celix_json_patch_remove(patch, "/a")); + EXPECT_EQ(0u, json_array_size(patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchRemovePathSetNewFail) { + //Given the second json_object_set_new (the "path" key) is injected to + //fail in patch_remove + json_auto_t* patch = json_array(); + celix_ei_expect_json_object_set_new((void*)celix_json_patch_remove, 0, -1, 2); + //Then removing fails and releases the op object + EXPECT_EQ(-1, celix_json_patch_remove(patch, "/a")); + EXPECT_EQ(0u, json_array_size(patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchRemoveAppendFail) { + //Given json_array_append_new is injected to fail in patch_remove; the + //failed append_new consumes the op + json_auto_t* patch = json_array(); + celix_ei_expect_json_array_append_new((void*)celix_json_patch_remove, 0, -1); + //Then removing fails and the patch stays empty + EXPECT_EQ(-1, celix_json_patch_remove(patch, "/a")); + EXPECT_EQ(0u, json_array_size(patch)); +} + +/* ── apply failures ────────────────────────────────────────────────────── */ + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchApplyRootReplaceDeepCopyFail) { + //Given deep-copy #1 (the initial copy of the original) succeeds and #2 + //(the root-replacement copy) is injected to fail in patch_apply + json_auto_t* original = json_loads("{}", 0, nullptr); + json_auto_t* patch = json_loads(R"([{"op":"add","path":"","value":1}])", 0, nullptr); + ASSERT_NE(nullptr, original); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_deep_copy((void*)celix_json_patch_apply, 0, nullptr, 2); + //Then applying fails; the error path decrefs the already-NULL result (no-op) + EXPECT_EQ(nullptr, celix_json_patch_apply(original, patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchApplyRemoveRootNullFail) { + //Given json_null is injected to fail for the root-removal null in patch_apply; + //the first json_null call of the apply + json_auto_t* original = json_loads("{}", 0, nullptr); + json_auto_t* patch = json_loads(R"([{"op":"remove","path":""}])", 0, nullptr); + ASSERT_NE(nullptr, original); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_null((void*)celix_json_patch_apply, 0, nullptr); + //Then applying fails; the error path decrefs the already-NULL result (no-op) + EXPECT_EQ(nullptr, celix_json_patch_apply(original, patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchApplyWalkObjectCreateFail) { + //Given json_object is injected to fail while creating the intermediate + //object "a" for path "/a/b" — the first json_object call of the apply + json_auto_t* original = json_loads("{}", 0, nullptr); + json_auto_t* patch = json_loads(R"([{"op":"add","path":"/a/b","value":1}])", 0, nullptr); + ASSERT_NE(nullptr, original); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_object((void*)celix_json_patch_apply, 0, nullptr); + //Then applying fails and the partially built result is released + EXPECT_EQ(nullptr, celix_json_patch_apply(original, patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchApplyWalkObjectSetNewFail) { + //Given json_object_set_new is injected to fail while inserting the + //intermediate object "a"; this is the first set_new of the apply call + //(the walk insert precedes the final write) + json_auto_t* original = json_loads("{}", 0, nullptr); + json_auto_t* patch = json_loads(R"([{"op":"add","path":"/a/b","value":1}])", 0, nullptr); + ASSERT_NE(nullptr, original); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_object_set_new((void*)celix_json_patch_apply, 0, -1); + //Then applying fails and the partially built result is released + EXPECT_EQ(nullptr, celix_json_patch_apply(original, patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchApplyWalkArrayChildCreateFail) { + //Given json_object is injected to fail while creating the child container + //at array index 0 (path "/a/0/b", original {"a":[]}) — the first + //json_object call of the apply + json_auto_t* original = json_loads(R"({"a":[]})", 0, nullptr); + json_auto_t* patch = json_loads(R"([{"op":"add","path":"/a/0/b","value":1}])", 0, nullptr); + ASSERT_NE(nullptr, original); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_object((void*)celix_json_patch_apply, 0, nullptr); + //Then applying fails and the partially built result is released + EXPECT_EQ(nullptr, celix_json_patch_apply(original, patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchApplyWalkArrayChildAppendFail) { + //Given json_array_append_new is injected to fail while appending the + //child container at array index 0; no padding precedes it (idx == size), + //so this is the first append_new of the apply call + json_auto_t* original = json_loads(R"({"a":[]})", 0, nullptr); + json_auto_t* patch = json_loads(R"([{"op":"add","path":"/a/0/b","value":1}])", 0, nullptr); + ASSERT_NE(nullptr, original); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_array_append_new((void*)celix_json_patch_apply, 0, -1); + //Then applying fails and the partially built result is released + EXPECT_EQ(nullptr, celix_json_patch_apply(original, patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchApplyWalkArrayPadNullFail) { + //Given json_null is injected to fail while padding index 2 of the walk + //array (path "/a/2/b") — the first json_null call of the apply + json_auto_t* original = json_loads(R"({"a":[]})", 0, nullptr); + json_auto_t* patch = json_loads(R"([{"op":"add","path":"/a/2/b","value":1}])", 0, nullptr); + ASSERT_NE(nullptr, original); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_null((void*)celix_json_patch_apply, 0, nullptr); + //Then applying fails instead of looping forever on the failed padding + EXPECT_EQ(nullptr, celix_json_patch_apply(original, patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchApplyWalkArrayPadAppendFail) { + //Given json_array_append_new is injected to fail while appending the + //padding null at index 2 of the walk array — the first append_new call + json_auto_t* original = json_loads(R"({"a":[]})", 0, nullptr); + json_auto_t* patch = json_loads(R"([{"op":"add","path":"/a/2/b","value":1}])", 0, nullptr); + ASSERT_NE(nullptr, original); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_array_append_new((void*)celix_json_patch_apply, 0, -1); + //Then applying fails instead of looping forever on the failed padding + EXPECT_EQ(nullptr, celix_json_patch_apply(original, patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchApplyFinalObjectAddWriteFail) { + //Given json_object_set_new is injected to fail in the final "add" write. + //The walk finds "a" already present, so no intermediate allocation + //happens and the final write is the first set_new of the apply call + json_auto_t* original = json_loads(R"({"a":{}})", 0, nullptr); + json_auto_t* patch = json_loads(R"([{"op":"add","path":"/a/b","value":1}])", 0, nullptr); + ASSERT_NE(nullptr, original); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_object_set_new((void*)celix_json_patch_apply, 0, -1); + //Then applying fails and the partially built result is released + EXPECT_EQ(nullptr, celix_json_patch_apply(original, patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchApplyFinalObjectReplaceWriteFail) { + //Given json_object_set_new is injected to fail in the final "replace" + //write; key "b" exists, so the replace branch takes the set_new and it + //is the first set_new of the apply call + json_auto_t* original = json_loads(R"({"a":{"b":0}})", 0, nullptr); + json_auto_t* patch = json_loads(R"([{"op":"replace","path":"/a/b","value":1}])", 0, nullptr); + ASSERT_NE(nullptr, original); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_object_set_new((void*)celix_json_patch_apply, 0, -1); + //Then applying fails and the partially built result is released + EXPECT_EQ(nullptr, celix_json_patch_apply(original, patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchApplyFinalArrayReplaceCopyFail) { + //Given deep-copy #1 (the initial copy of the original) succeeds and #2 + //(the array-replace value copy for "/a/1" on [1,2]) is injected to fail + json_auto_t* original = json_loads(R"({"a":[1,2]})", 0, nullptr); + json_auto_t* patch = json_loads(R"([{"op":"replace","path":"/a/1","value":3}])", 0, nullptr); + ASSERT_NE(nullptr, original); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_deep_copy((void*)celix_json_patch_apply, 0, nullptr, 2); + //Then applying fails and the partially built result is released + EXPECT_EQ(nullptr, celix_json_patch_apply(original, patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchApplyFinalArrayReplaceSetNewFail) { + //Given json_array_set_new is injected to fail for the array-replace write + //("/a/1" on [1,2]); the first json_array_set_new of the apply call + json_auto_t* original = json_loads(R"({"a":[1,2]})", 0, nullptr); + json_auto_t* patch = json_loads(R"([{"op":"replace","path":"/a/1","value":3}])", 0, nullptr); + ASSERT_NE(nullptr, original); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_array_set_new((void*)celix_json_patch_apply, 0, -1); + //Then applying fails and the partially built result is released + EXPECT_EQ(nullptr, celix_json_patch_apply(original, patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchApplyFinalArrayInsertCopyFail) { + //Given deep-copy #1 (the initial copy of the original) succeeds and #2 + //(the mid-array insert copy for "/a/1" on [1,2] — idx != size, so insert) + //is injected to fail + json_auto_t* original = json_loads(R"({"a":[1,2]})", 0, nullptr); + json_auto_t* patch = json_loads(R"([{"op":"add","path":"/a/1","value":3}])", 0, nullptr); + ASSERT_NE(nullptr, original); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_deep_copy((void*)celix_json_patch_apply, 0, nullptr, 2); + //Then applying fails and the partially built result is released + EXPECT_EQ(nullptr, celix_json_patch_apply(original, patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchApplyFinalArrayInsertNewFail) { + //Given json_array_insert_new is injected to fail for the mid-array "add" + //("/a/1" on [1,2] — idx != size, so insert); the first json_array_insert_new + //of the apply call + json_auto_t* original = json_loads(R"({"a":[1,2]})", 0, nullptr); + json_auto_t* patch = json_loads(R"([{"op":"add","path":"/a/1","value":3}])", 0, nullptr); + ASSERT_NE(nullptr, original); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_array_insert_new((void*)celix_json_patch_apply, 0, -1); + //Then applying fails and the partially built result is released + EXPECT_EQ(nullptr, celix_json_patch_apply(original, patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchApplyFinalArrayAppendFail) { + //Given json_array_append_new is injected to fail for the end-of-array + //"add" ("/a/2" on [1,2] — idx == size, so append); no padding precedes + //it, so this is the first append_new of the apply call + json_auto_t* original = json_loads(R"({"a":[1,2]})", 0, nullptr); + json_auto_t* patch = json_loads(R"([{"op":"add","path":"/a/2","value":3}])", 0, nullptr); + ASSERT_NE(nullptr, original); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_array_append_new((void*)celix_json_patch_apply, 0, -1); + //Then applying fails and the partially built result is released + EXPECT_EQ(nullptr, celix_json_patch_apply(original, patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchApplyFinalArrayPadNullFail) { + //Given json_null is injected to fail while padding index 2 of the + //final "add" (path "/a/2", original {"a":[]}) — the first json_null call + json_auto_t* original = json_loads(R"({"a":[]})", 0, nullptr); + json_auto_t* patch = json_loads(R"([{"op":"add","path":"/a/2","value":3}])", 0, nullptr); + ASSERT_NE(nullptr, original); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_null((void*)celix_json_patch_apply, 0, nullptr); + //Then applying fails instead of looping forever on the failed padding + EXPECT_EQ(nullptr, celix_json_patch_apply(original, patch)); +} + +TEST_F(JanssonExtErrorInjectionTestSuite, PatchApplyFinalArrayPadAppendFail) { + //Given json_array_append_new is injected to fail while appending the + //padding null at index 2 of the final "add" — the first append_new call + json_auto_t* original = json_loads(R"({"a":[]})", 0, nullptr); + json_auto_t* patch = json_loads(R"([{"op":"add","path":"/a/2","value":3}])", 0, nullptr); + ASSERT_NE(nullptr, original); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_array_append_new((void*)celix_json_patch_apply, 0, -1); + //Then applying fails instead of looping forever on the failed padding + EXPECT_EQ(nullptr, celix_json_patch_apply(original, patch)); +} diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/baseUriChange/folderInteger.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/baseUriChange/folderInteger.json new file mode 100644 index 000000000..e0b872812 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/baseUriChange/folderInteger.json @@ -0,0 +1 @@ +{"type": "integer"} diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/baseUriChangeFolder/folderInteger.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/baseUriChangeFolder/folderInteger.json new file mode 100644 index 000000000..e0b872812 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/baseUriChangeFolder/folderInteger.json @@ -0,0 +1 @@ +{"type": "integer"} diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/baseUriChangeFolderInSubschema/folderInteger.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/baseUriChangeFolderInSubschema/folderInteger.json new file mode 100644 index 000000000..e0b872812 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/baseUriChangeFolderInSubschema/folderInteger.json @@ -0,0 +1 @@ +{"type": "integer"} diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/integer.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/integer.json new file mode 100644 index 000000000..8b50ea308 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/integer.json @@ -0,0 +1,3 @@ +{ + "type": "integer" +} diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/name-defs.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/name-defs.json new file mode 100644 index 000000000..1dab4a434 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/name-defs.json @@ -0,0 +1,15 @@ +{ + "$defs": { + "orNull": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#" + } + ] + } + }, + "type": "string" +} diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/name.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/name.json new file mode 100644 index 000000000..fceacb809 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/name.json @@ -0,0 +1,15 @@ +{ + "definitions": { + "orNull": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#" + } + ] + } + }, + "type": "string" +} diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/ref-and-definitions.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/ref-and-definitions.json new file mode 100644 index 000000000..e0ee802a9 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/ref-and-definitions.json @@ -0,0 +1,11 @@ +{ + "$id": "http://localhost:1234/ref-and-definitions.json", + "definitions": { + "inner": { + "properties": { + "bar": { "type": "string" } + } + } + }, + "allOf": [ { "$ref": "#/definitions/inner" } ] +} diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/ref-and-defs.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/ref-and-defs.json new file mode 100644 index 000000000..85d06c399 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/ref-and-defs.json @@ -0,0 +1,11 @@ +{ + "$id": "http://localhost:1234/ref-and-defs.json", + "$defs": { + "inner": { + "properties": { + "bar": { "type": "string" } + } + } + }, + "$ref": "#/$defs/inner" +} diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/subSchemas-defs.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/subSchemas-defs.json new file mode 100644 index 000000000..50b7b6dc4 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/subSchemas-defs.json @@ -0,0 +1,10 @@ +{ + "$defs": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/$defs/integer" + } + } +} diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/subSchemas.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/subSchemas.json new file mode 100644 index 000000000..9f8030bce --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/remotes/subSchemas.json @@ -0,0 +1,8 @@ +{ + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/integer" + } +} diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/additionalItems.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/additionalItems.json new file mode 100644 index 000000000..784bc8461 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/additionalItems.json @@ -0,0 +1,149 @@ +[ + { + "description": "additionalItems as schema", + "schema": { + "items": [{}], + "additionalItems": {"type": "integer"} + }, + "tests": [ + { + "description": "additional items match schema", + "data": [ null, 2, 3, 4 ], + "valid": true + }, + { + "description": "additional items do not match schema", + "data": [ null, 2, 3, "foo" ], + "valid": false + } + ] + }, + { + "description": "when items is schema, additionalItems does nothing", + "schema": { + "items": {}, + "additionalItems": false + }, + "tests": [ + { + "description": "all items match schema", + "data": [ 1, 2, 3, 4, 5 ], + "valid": true + } + ] + }, + { + "description": "array of items with no additionalItems permitted", + "schema": { + "items": [{}, {}, {}], + "additionalItems": false + }, + "tests": [ + { + "description": "empty array", + "data": [ ], + "valid": true + }, + { + "description": "fewer number of items present (1)", + "data": [ 1 ], + "valid": true + }, + { + "description": "fewer number of items present (2)", + "data": [ 1, 2 ], + "valid": true + }, + { + "description": "equal number of items present", + "data": [ 1, 2, 3 ], + "valid": true + }, + { + "description": "additional items are not permitted", + "data": [ 1, 2, 3, 4 ], + "valid": false + } + ] + }, + { + "description": "additionalItems as false without items", + "schema": {"additionalItems": false}, + "tests": [ + { + "description": + "items defaults to empty schema so everything is valid", + "data": [ 1, 2, 3, 4, 5 ], + "valid": true + }, + { + "description": "ignores non-arrays", + "data": {"foo" : "bar"}, + "valid": true + } + ] + }, + { + "description": "additionalItems are allowed by default", + "schema": {"items": [{"type": "integer"}]}, + "tests": [ + { + "description": "only the first item is validated", + "data": [1, "foo", false], + "valid": true + } + ] + }, + { + "description": "additionalItems should not look in applicators, valid case", + "schema": { + "allOf": [ + { "items": [ { "type": "integer" } ] } + ], + "additionalItems": { "type": "boolean" } + }, + "tests": [ + { + "description": "items defined in allOf are not examined", + "data": [ 1, null ], + "valid": true + } + ] + }, + { + "description": "additionalItems should not look in applicators, invalid case", + "schema": { + "allOf": [ + { "items": [ { "type": "integer" }, { "type": "string" } ] } + ], + "items": [ {"type": "integer" } ], + "additionalItems": { "type": "boolean" } + }, + "tests": [ + { + "description": "items defined in allOf are not examined", + "data": [ 1, "hello" ], + "valid": false + } + ] + }, + { + "description": "items validation adjusts the starting index for additionalItems", + "schema": { + "items": [ { "type": "string" } ], + "additionalItems": { "type": "integer" } + }, + "tests": [ + { + "description": "valid items", + "data": [ "x", 2, 3 ], + "valid": true + }, + { + "description": "wrong type of second item", + "data": [ "x", "y" ], + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/additionalProperties.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/additionalProperties.json new file mode 100644 index 000000000..381275a59 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/additionalProperties.json @@ -0,0 +1,133 @@ +[ + { + "description": + "additionalProperties being false does not allow other properties", + "schema": { + "properties": {"foo": {}, "bar": {}}, + "patternProperties": { "^v": {} }, + "additionalProperties": false + }, + "tests": [ + { + "description": "no additional properties is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "an additional property is invalid", + "data": {"foo" : 1, "bar" : 2, "quux" : "boom"}, + "valid": false + }, + { + "description": "ignores arrays", + "data": [1, 2, 3], + "valid": true + }, + { + "description": "ignores strings", + "data": "foobarbaz", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + }, + { + "description": "patternProperties are not additional properties", + "data": {"foo":1, "vroom": 2}, + "valid": true + } + ] + }, + { + "description": "non-ASCII pattern with additionalProperties", + "schema": { + "patternProperties": {"^á": {}}, + "additionalProperties": false + }, + "tests": [ + { + "description": "matching the pattern is valid", + "data": {"ármányos": 2}, + "valid": true + }, + { + "description": "not matching the pattern is invalid", + "data": {"élmény": 2}, + "valid": false + } + ] + }, + { + "description": + "additionalProperties allows a schema which should validate", + "schema": { + "properties": {"foo": {}, "bar": {}}, + "additionalProperties": {"type": "boolean"} + }, + "tests": [ + { + "description": "no additional properties is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "an additional valid property is valid", + "data": {"foo" : 1, "bar" : 2, "quux" : true}, + "valid": true + }, + { + "description": "an additional invalid property is invalid", + "data": {"foo" : 1, "bar" : 2, "quux" : 12}, + "valid": false + } + ] + }, + { + "description": + "additionalProperties can exist by itself", + "schema": { + "additionalProperties": {"type": "boolean"} + }, + "tests": [ + { + "description": "an additional valid property is valid", + "data": {"foo" : true}, + "valid": true + }, + { + "description": "an additional invalid property is invalid", + "data": {"foo" : 1}, + "valid": false + } + ] + }, + { + "description": "additionalProperties are allowed by default", + "schema": {"properties": {"foo": {}, "bar": {}}}, + "tests": [ + { + "description": "additional properties are allowed", + "data": {"foo": 1, "bar": 2, "quux": true}, + "valid": true + } + ] + }, + { + "description": "additionalProperties should not look in applicators", + "schema": { + "allOf": [ + {"properties": {"foo": {}}} + ], + "additionalProperties": {"type": "boolean"} + }, + "tests": [ + { + "description": "properties defined in allOf are not examined", + "data": {"foo": 1, "bar": true}, + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/allOf.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/allOf.json new file mode 100644 index 000000000..ec9319e14 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/allOf.json @@ -0,0 +1,294 @@ +[ + { + "description": "allOf", + "schema": { + "allOf": [ + { + "properties": { + "bar": {"type": "integer"} + }, + "required": ["bar"] + }, + { + "properties": { + "foo": {"type": "string"} + }, + "required": ["foo"] + } + ] + }, + "tests": [ + { + "description": "allOf", + "data": {"foo": "baz", "bar": 2}, + "valid": true + }, + { + "description": "mismatch second", + "data": {"foo": "baz"}, + "valid": false + }, + { + "description": "mismatch first", + "data": {"bar": 2}, + "valid": false + }, + { + "description": "wrong type", + "data": {"foo": "baz", "bar": "quux"}, + "valid": false + } + ] + }, + { + "description": "allOf with base schema", + "schema": { + "properties": {"bar": {"type": "integer"}}, + "required": ["bar"], + "allOf" : [ + { + "properties": { + "foo": {"type": "string"} + }, + "required": ["foo"] + }, + { + "properties": { + "baz": {"type": "null"} + }, + "required": ["baz"] + } + ] + }, + "tests": [ + { + "description": "valid", + "data": {"foo": "quux", "bar": 2, "baz": null}, + "valid": true + }, + { + "description": "mismatch base schema", + "data": {"foo": "quux", "baz": null}, + "valid": false + }, + { + "description": "mismatch first allOf", + "data": {"bar": 2, "baz": null}, + "valid": false + }, + { + "description": "mismatch second allOf", + "data": {"foo": "quux", "bar": 2}, + "valid": false + }, + { + "description": "mismatch both", + "data": {"bar": 2}, + "valid": false + } + ] + }, + { + "description": "allOf simple types", + "schema": { + "allOf": [ + {"maximum": 30}, + {"minimum": 20} + ] + }, + "tests": [ + { + "description": "valid", + "data": 25, + "valid": true + }, + { + "description": "mismatch one", + "data": 35, + "valid": false + } + ] + }, + { + "description": "allOf with boolean schemas, all true", + "schema": {"allOf": [true, true]}, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "allOf with boolean schemas, some false", + "schema": {"allOf": [true, false]}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "allOf with boolean schemas, all false", + "schema": {"allOf": [false, false]}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "allOf with one empty schema", + "schema": { + "allOf": [ + {} + ] + }, + "tests": [ + { + "description": "any data is valid", + "data": 1, + "valid": true + } + ] + }, + { + "description": "allOf with two empty schemas", + "schema": { + "allOf": [ + {}, + {} + ] + }, + "tests": [ + { + "description": "any data is valid", + "data": 1, + "valid": true + } + ] + }, + { + "description": "allOf with the first empty schema", + "schema": { + "allOf": [ + {}, + { "type": "number" } + ] + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "allOf with the last empty schema", + "schema": { + "allOf": [ + { "type": "number" }, + {} + ] + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "nested allOf, to check validation semantics", + "schema": { + "allOf": [ + { + "allOf": [ + { + "type": "null" + } + ] + } + ] + }, + "tests": [ + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "anything non-null is invalid", + "data": 123, + "valid": false + } + ] + }, + { + "description": "allOf combined with anyOf, oneOf", + "schema": { + "allOf": [ { "multipleOf": 2 } ], + "anyOf": [ { "multipleOf": 3 } ], + "oneOf": [ { "multipleOf": 5 } ] + }, + "tests": [ + { + "description": "allOf: false, anyOf: false, oneOf: false", + "data": 1, + "valid": false + }, + { + "description": "allOf: false, anyOf: false, oneOf: true", + "data": 5, + "valid": false + }, + { + "description": "allOf: false, anyOf: true, oneOf: false", + "data": 3, + "valid": false + }, + { + "description": "allOf: false, anyOf: true, oneOf: true", + "data": 15, + "valid": false + }, + { + "description": "allOf: true, anyOf: false, oneOf: false", + "data": 2, + "valid": false + }, + { + "description": "allOf: true, anyOf: false, oneOf: true", + "data": 10, + "valid": false + }, + { + "description": "allOf: true, anyOf: true, oneOf: false", + "data": 6, + "valid": false + }, + { + "description": "allOf: true, anyOf: true, oneOf: true", + "data": 30, + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/anyOf.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/anyOf.json new file mode 100644 index 000000000..b720afa8d --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/anyOf.json @@ -0,0 +1,215 @@ +[ + { + "description": "anyOf", + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "minimum": 2 + } + ] + }, + "tests": [ + { + "description": "first anyOf valid", + "data": 1, + "valid": true + }, + { + "description": "second anyOf valid", + "data": 2.5, + "valid": true + }, + { + "description": "both anyOf valid", + "data": 3, + "valid": true + }, + { + "description": "neither anyOf valid", + "data": 1.5, + "valid": false + } + ] + }, + { + "description": "anyOf with base schema", + "schema": { + "type": "string", + "anyOf" : [ + { + "maxLength": 2 + }, + { + "minLength": 4 + } + ] + }, + "tests": [ + { + "description": "mismatch base schema", + "data": 3, + "valid": false + }, + { + "description": "one anyOf valid", + "data": "foobar", + "valid": true + }, + { + "description": "both anyOf invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "anyOf with boolean schemas, all true", + "schema": {"anyOf": [true, true]}, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "anyOf with boolean schemas, some true", + "schema": {"anyOf": [true, false]}, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "anyOf with boolean schemas, all false", + "schema": {"anyOf": [false, false]}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "anyOf complex types", + "schema": { + "anyOf": [ + { + "properties": { + "bar": {"type": "integer"} + }, + "required": ["bar"] + }, + { + "properties": { + "foo": {"type": "string"} + }, + "required": ["foo"] + } + ] + }, + "tests": [ + { + "description": "first anyOf valid (complex)", + "data": {"bar": 2}, + "valid": true + }, + { + "description": "second anyOf valid (complex)", + "data": {"foo": "baz"}, + "valid": true + }, + { + "description": "both anyOf valid (complex)", + "data": {"foo": "baz", "bar": 2}, + "valid": true + }, + { + "description": "neither anyOf valid (complex)", + "data": {"foo": 2, "bar": "quux"}, + "valid": false + } + ] + }, + { + "description": "anyOf with one empty schema", + "schema": { + "anyOf": [ + { "type": "number" }, + {} + ] + }, + "tests": [ + { + "description": "string is valid", + "data": "foo", + "valid": true + }, + { + "description": "number is valid", + "data": 123, + "valid": true + } + ] + }, + { + "description": "nested anyOf, to check validation semantics", + "schema": { + "anyOf": [ + { + "anyOf": [ + { + "type": "null" + } + ] + } + ] + }, + "tests": [ + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "anything non-null is invalid", + "data": 123, + "valid": false + } + ] + }, + { + "description": "nested anyOf, to check validation semantics", + "schema": { + "anyOf": [ + { + "anyOf": [ + { + "type": "null" + } + ] + } + ] + }, + "tests": [ + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "anything non-null is invalid", + "data": 123, + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/boolean_schema.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/boolean_schema.json new file mode 100644 index 000000000..6d40f23f2 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/boolean_schema.json @@ -0,0 +1,104 @@ +[ + { + "description": "boolean schema 'true'", + "schema": true, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "string is valid", + "data": "foo", + "valid": true + }, + { + "description": "boolean true is valid", + "data": true, + "valid": true + }, + { + "description": "boolean false is valid", + "data": false, + "valid": true + }, + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "object is valid", + "data": {"foo": "bar"}, + "valid": true + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + }, + { + "description": "array is valid", + "data": ["foo"], + "valid": true + }, + { + "description": "empty array is valid", + "data": [], + "valid": true + } + ] + }, + { + "description": "boolean schema 'false'", + "schema": false, + "tests": [ + { + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/const.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/const.json new file mode 100644 index 000000000..1c2cafcc1 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/const.json @@ -0,0 +1,342 @@ +[ + { + "description": "const validation", + "schema": {"const": 2}, + "tests": [ + { + "description": "same value is valid", + "data": 2, + "valid": true + }, + { + "description": "another value is invalid", + "data": 5, + "valid": false + }, + { + "description": "another type is invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "const with object", + "schema": {"const": {"foo": "bar", "baz": "bax"}}, + "tests": [ + { + "description": "same object is valid", + "data": {"foo": "bar", "baz": "bax"}, + "valid": true + }, + { + "description": "same object with different property order is valid", + "data": {"baz": "bax", "foo": "bar"}, + "valid": true + }, + { + "description": "another object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "another type is invalid", + "data": [1, 2], + "valid": false + } + ] + }, + { + "description": "const with array", + "schema": {"const": [{ "foo": "bar" }]}, + "tests": [ + { + "description": "same array is valid", + "data": [{"foo": "bar"}], + "valid": true + }, + { + "description": "another array item is invalid", + "data": [2], + "valid": false + }, + { + "description": "array with additional items is invalid", + "data": [1, 2, 3], + "valid": false + } + ] + }, + { + "description": "const with null", + "schema": {"const": null}, + "tests": [ + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "not null is invalid", + "data": 0, + "valid": false + } + ] + }, + { + "description": "const with false does not match 0", + "schema": {"const": false}, + "tests": [ + { + "description": "false is valid", + "data": false, + "valid": true + }, + { + "description": "integer zero is invalid", + "data": 0, + "valid": false + }, + { + "description": "float zero is invalid", + "data": 0.0, + "valid": false + } + ] + }, + { + "description": "const with true does not match 1", + "schema": {"const": true}, + "tests": [ + { + "description": "true is valid", + "data": true, + "valid": true + }, + { + "description": "integer one is invalid", + "data": 1, + "valid": false + }, + { + "description": "float one is invalid", + "data": 1.0, + "valid": false + } + ] + }, + { + "description": "const with [false] does not match [0]", + "schema": {"const": [false]}, + "tests": [ + { + "description": "[false] is valid", + "data": [false], + "valid": true + }, + { + "description": "[0] is invalid", + "data": [0], + "valid": false + }, + { + "description": "[0.0] is invalid", + "data": [0.0], + "valid": false + } + ] + }, + { + "description": "const with [true] does not match [1]", + "schema": {"const": [true]}, + "tests": [ + { + "description": "[true] is valid", + "data": [true], + "valid": true + }, + { + "description": "[1] is invalid", + "data": [1], + "valid": false + }, + { + "description": "[1.0] is invalid", + "data": [1.0], + "valid": false + } + ] + }, + { + "description": "const with {\"a\": false} does not match {\"a\": 0}", + "schema": {"const": {"a": false}}, + "tests": [ + { + "description": "{\"a\": false} is valid", + "data": {"a": false}, + "valid": true + }, + { + "description": "{\"a\": 0} is invalid", + "data": {"a": 0}, + "valid": false + }, + { + "description": "{\"a\": 0.0} is invalid", + "data": {"a": 0.0}, + "valid": false + } + ] + }, + { + "description": "const with {\"a\": true} does not match {\"a\": 1}", + "schema": {"const": {"a": true}}, + "tests": [ + { + "description": "{\"a\": true} is valid", + "data": {"a": true}, + "valid": true + }, + { + "description": "{\"a\": 1} is invalid", + "data": {"a": 1}, + "valid": false + }, + { + "description": "{\"a\": 1.0} is invalid", + "data": {"a": 1.0}, + "valid": false + } + ] + }, + { + "description": "const with 0 does not match other zero-like types", + "schema": {"const": 0}, + "tests": [ + { + "description": "false is invalid", + "data": false, + "valid": false + }, + { + "description": "integer zero is valid", + "data": 0, + "valid": true + }, + { + "description": "float zero is valid", + "data": 0.0, + "valid": true + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + }, + { + "description": "empty string is invalid", + "data": "", + "valid": false + } + ] + }, + { + "description": "const with 1 does not match true", + "schema": {"const": 1}, + "tests": [ + { + "description": "true is invalid", + "data": true, + "valid": false + }, + { + "description": "integer one is valid", + "data": 1, + "valid": true + }, + { + "description": "float one is valid", + "data": 1.0, + "valid": true + } + ] + }, + { + "description": "const with -2.0 matches integer and float types", + "schema": {"const": -2.0}, + "tests": [ + { + "description": "integer -2 is valid", + "data": -2, + "valid": true + }, + { + "description": "integer 2 is invalid", + "data": 2, + "valid": false + }, + { + "description": "float -2.0 is valid", + "data": -2.0, + "valid": true + }, + { + "description": "float 2.0 is invalid", + "data": 2.0, + "valid": false + }, + { + "description": "float -2.00001 is invalid", + "data": -2.00001, + "valid": false + } + ] + }, + { + "description": "float and integers are equal up to 64-bit representation limits", + "schema": {"const": 9007199254740992}, + "tests": [ + { + "description": "integer is valid", + "data": 9007199254740992, + "valid": true + }, + { + "description": "integer minus one is invalid", + "data": 9007199254740991, + "valid": false + }, + { + "description": "float is valid", + "data": 9007199254740992.0, + "valid": true + }, + { + "description": "float minus one is invalid", + "data": 9007199254740991.0, + "valid": false + } + ] + }, + { + "description": "nul characters in strings", + "schema": { "const": "hello\u0000there" }, + "tests": [ + { + "description": "match string with nul", + "data": "hello\u0000there", + "valid": true + }, + { + "description": "do not match string lacking nul", + "data": "hellothere", + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/contains.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/contains.json new file mode 100644 index 000000000..215da98e8 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/contains.json @@ -0,0 +1,150 @@ +[ + { + "description": "contains keyword validation", + "schema": { + "contains": {"minimum": 5} + }, + "tests": [ + { + "description": "array with item matching schema (5) is valid", + "data": [3, 4, 5], + "valid": true + }, + { + "description": "array with item matching schema (6) is valid", + "data": [3, 4, 6], + "valid": true + }, + { + "description": "array with two items matching schema (5, 6) is valid", + "data": [3, 4, 5, 6], + "valid": true + }, + { + "description": "array without items matching schema is invalid", + "data": [2, 3, 4], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + }, + { + "description": "not array is valid", + "data": {}, + "valid": true + } + ] + }, + { + "description": "contains keyword with const keyword", + "schema": { + "contains": { "const": 5 } + }, + "tests": [ + { + "description": "array with item 5 is valid", + "data": [3, 4, 5], + "valid": true + }, + { + "description": "array with two items 5 is valid", + "data": [3, 4, 5, 5], + "valid": true + }, + { + "description": "array without item 5 is invalid", + "data": [1, 2, 3, 4], + "valid": false + } + ] + }, + { + "description": "contains keyword with boolean schema true", + "schema": {"contains": true}, + "tests": [ + { + "description": "any non-empty array is valid", + "data": ["foo"], + "valid": true + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + }, + { + "description": "contains keyword with boolean schema false", + "schema": {"contains": false}, + "tests": [ + { + "description": "any non-empty array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + }, + { + "description": "non-arrays are valid", + "data": "contains does not apply to strings", + "valid": true + } + ] + }, + { + "description": "items + contains", + "schema": { + "items": { "multipleOf": 2 }, + "contains": { "multipleOf": 3 } + }, + "tests": [ + { + "description": "matches items, does not match contains", + "data": [ 2, 4, 8 ], + "valid": false + }, + { + "description": "does not match items, matches contains", + "data": [ 3, 6, 9 ], + "valid": false + }, + { + "description": "matches both items and contains", + "data": [ 6, 12 ], + "valid": true + }, + { + "description": "matches neither items nor contains", + "data": [ 1, 5 ], + "valid": false + } + ] + }, + { + "description": "contains with false if subschema", + "schema": { + "contains": { + "if": false, + "else": true + } + }, + "tests": [ + { + "description": "any non-empty array is valid", + "data": ["foo"], + "valid": true + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/default.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/default.json new file mode 100644 index 000000000..289a9b66c --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/default.json @@ -0,0 +1,79 @@ +[ + { + "description": "invalid type for default", + "schema": { + "properties": { + "foo": { + "type": "integer", + "default": [] + } + } + }, + "tests": [ + { + "description": "valid when property is specified", + "data": {"foo": 13}, + "valid": true + }, + { + "description": "still valid when the invalid default is used", + "data": {}, + "valid": true + } + ] + }, + { + "description": "invalid string value for default", + "schema": { + "properties": { + "bar": { + "type": "string", + "minLength": 4, + "default": "bad" + } + } + }, + "tests": [ + { + "description": "valid when property is specified", + "data": {"bar": "good"}, + "valid": true + }, + { + "description": "still valid when the invalid default is used", + "data": {}, + "valid": true + } + ] + }, + { + "description": "the default keyword does not do anything if the property is missing", + "schema": { + "type": "object", + "properties": { + "alpha": { + "type": "number", + "maximum": 3, + "default": 5 + } + } + }, + "tests": [ + { + "description": "an explicit property value is checked against maximum (passing)", + "data": { "alpha": 1 }, + "valid": true + }, + { + "description": "an explicit property value is checked against maximum (failing)", + "data": { "alpha": 5 }, + "valid": false + }, + { + "description": "missing properties are not filled in with the default", + "data": {}, + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/definitions.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/definitions.json new file mode 100644 index 000000000..afe396e42 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/definitions.json @@ -0,0 +1,26 @@ +[ + { + "description": "validate definition against metaschema", + "schema": {"$ref": "http://json-schema.org/draft-07/schema#"}, + "tests": [ + { + "description": "valid definition schema", + "data": { + "definitions": { + "foo": {"type": "integer"} + } + }, + "valid": true + }, + { + "description": "invalid definition schema", + "data": { + "definitions": { + "foo": {"type": 1} + } + }, + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/dependencies.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/dependencies.json new file mode 100644 index 000000000..a5e54282c --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/dependencies.json @@ -0,0 +1,248 @@ +[ + { + "description": "dependencies", + "schema": { + "dependencies": {"bar": ["foo"]} + }, + "tests": [ + { + "description": "neither", + "data": {}, + "valid": true + }, + { + "description": "nondependant", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "with dependency", + "data": {"foo": 1, "bar": 2}, + "valid": true + }, + { + "description": "missing dependency", + "data": {"bar": 2}, + "valid": false + }, + { + "description": "ignores arrays", + "data": ["bar"], + "valid": true + }, + { + "description": "ignores strings", + "data": "foobar", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + } + ] + }, + { + "description": "dependencies with empty array", + "schema": { + "dependencies": {"bar": []} + }, + "tests": [ + { + "description": "empty object", + "data": {}, + "valid": true + }, + { + "description": "object with one property", + "data": {"bar": 2}, + "valid": true + }, + { + "description": "non-object is valid", + "data": 1, + "valid": true + } + ] + }, + { + "description": "multiple dependencies", + "schema": { + "dependencies": {"quux": ["foo", "bar"]} + }, + "tests": [ + { + "description": "neither", + "data": {}, + "valid": true + }, + { + "description": "nondependants", + "data": {"foo": 1, "bar": 2}, + "valid": true + }, + { + "description": "with dependencies", + "data": {"foo": 1, "bar": 2, "quux": 3}, + "valid": true + }, + { + "description": "missing dependency", + "data": {"foo": 1, "quux": 2}, + "valid": false + }, + { + "description": "missing other dependency", + "data": {"bar": 1, "quux": 2}, + "valid": false + }, + { + "description": "missing both dependencies", + "data": {"quux": 1}, + "valid": false + } + ] + }, + { + "description": "multiple dependencies subschema", + "schema": { + "dependencies": { + "bar": { + "properties": { + "foo": {"type": "integer"}, + "bar": {"type": "integer"} + } + } + } + }, + "tests": [ + { + "description": "valid", + "data": {"foo": 1, "bar": 2}, + "valid": true + }, + { + "description": "no dependency", + "data": {"foo": "quux"}, + "valid": true + }, + { + "description": "wrong type", + "data": {"foo": "quux", "bar": 2}, + "valid": false + }, + { + "description": "wrong type other", + "data": {"foo": 2, "bar": "quux"}, + "valid": false + }, + { + "description": "wrong type both", + "data": {"foo": "quux", "bar": "quux"}, + "valid": false + } + ] + }, + { + "description": "dependencies with boolean subschemas", + "schema": { + "dependencies": { + "foo": true, + "bar": false + } + }, + "tests": [ + { + "description": "object with property having schema true is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "object with property having schema false is invalid", + "data": {"bar": 2}, + "valid": false + }, + { + "description": "object with both properties is invalid", + "data": {"foo": 1, "bar": 2}, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] + }, + { + "description": "dependencies with escaped characters", + "schema": { + "dependencies": { + "foo\nbar": ["foo\rbar"], + "foo\tbar": { + "minProperties": 4 + }, + "foo'bar": {"required": ["foo\"bar"]}, + "foo\"bar": ["foo'bar"] + } + }, + "tests": [ + { + "description": "valid object 1", + "data": { + "foo\nbar": 1, + "foo\rbar": 2 + }, + "valid": true + }, + { + "description": "valid object 2", + "data": { + "foo\tbar": 1, + "a": 2, + "b": 3, + "c": 4 + }, + "valid": true + }, + { + "description": "valid object 3", + "data": { + "foo'bar": 1, + "foo\"bar": 2 + }, + "valid": true + }, + { + "description": "invalid object 1", + "data": { + "foo\nbar": 1, + "foo": 2 + }, + "valid": false + }, + { + "description": "invalid object 2", + "data": { + "foo\tbar": 1, + "a": 2 + }, + "valid": false + }, + { + "description": "invalid object 3", + "data": { + "foo'bar": 1 + }, + "valid": false + }, + { + "description": "invalid object 4", + "data": { + "foo\"bar": 2 + }, + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/enum.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/enum.json new file mode 100644 index 000000000..f085097be --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/enum.json @@ -0,0 +1,236 @@ +[ + { + "description": "simple enum validation", + "schema": {"enum": [1, 2, 3]}, + "tests": [ + { + "description": "one of the enum is valid", + "data": 1, + "valid": true + }, + { + "description": "something else is invalid", + "data": 4, + "valid": false + } + ] + }, + { + "description": "heterogeneous enum validation", + "schema": {"enum": [6, "foo", [], true, {"foo": 12}]}, + "tests": [ + { + "description": "one of the enum is valid", + "data": [], + "valid": true + }, + { + "description": "something else is invalid", + "data": null, + "valid": false + }, + { + "description": "objects are deep compared", + "data": {"foo": false}, + "valid": false + }, + { + "description": "valid object matches", + "data": {"foo": 12}, + "valid": true + }, + { + "description": "extra properties in object is invalid", + "data": {"foo": 12, "boo": 42}, + "valid": false + } + ] + }, + { + "description": "heterogeneous enum-with-null validation", + "schema": { "enum": [6, null] }, + "tests": [ + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "number is valid", + "data": 6, + "valid": true + }, + { + "description": "something else is invalid", + "data": "test", + "valid": false + } + ] + }, + { + "description": "enums in properties", + "schema": { + "type":"object", + "properties": { + "foo": {"enum":["foo"]}, + "bar": {"enum":["bar"]} + }, + "required": ["bar"] + }, + "tests": [ + { + "description": "both properties are valid", + "data": {"foo":"foo", "bar":"bar"}, + "valid": true + }, + { + "description": "wrong foo value", + "data": {"foo":"foot", "bar":"bar"}, + "valid": false + }, + { + "description": "wrong bar value", + "data": {"foo":"foo", "bar":"bart"}, + "valid": false + }, + { + "description": "missing optional property is valid", + "data": {"bar":"bar"}, + "valid": true + }, + { + "description": "missing required property is invalid", + "data": {"foo":"foo"}, + "valid": false + }, + { + "description": "missing all properties is invalid", + "data": {}, + "valid": false + } + ] + }, + { + "description": "enum with escaped characters", + "schema": { + "enum": ["foo\nbar", "foo\rbar"] + }, + "tests": [ + { + "description": "member 1 is valid", + "data": "foo\nbar", + "valid": true + }, + { + "description": "member 2 is valid", + "data": "foo\rbar", + "valid": true + }, + { + "description": "another string is invalid", + "data": "abc", + "valid": false + } + ] + }, + { + "description": "enum with false does not match 0", + "schema": {"enum": [false]}, + "tests": [ + { + "description": "false is valid", + "data": false, + "valid": true + }, + { + "description": "integer zero is invalid", + "data": 0, + "valid": false + }, + { + "description": "float zero is invalid", + "data": 0.0, + "valid": false + } + ] + }, + { + "description": "enum with true does not match 1", + "schema": {"enum": [true]}, + "tests": [ + { + "description": "true is valid", + "data": true, + "valid": true + }, + { + "description": "integer one is invalid", + "data": 1, + "valid": false + }, + { + "description": "float one is invalid", + "data": 1.0, + "valid": false + } + ] + }, + { + "description": "enum with 0 does not match false", + "schema": {"enum": [0]}, + "tests": [ + { + "description": "false is invalid", + "data": false, + "valid": false + }, + { + "description": "integer zero is valid", + "data": 0, + "valid": true + }, + { + "description": "float zero is valid", + "data": 0.0, + "valid": true + } + ] + }, + { + "description": "enum with 1 does not match true", + "schema": {"enum": [1]}, + "tests": [ + { + "description": "true is invalid", + "data": true, + "valid": false + }, + { + "description": "integer one is valid", + "data": 1, + "valid": true + }, + { + "description": "float one is valid", + "data": 1.0, + "valid": true + } + ] + }, + { + "description": "nul characters in strings", + "schema": { "enum": [ "hello\u0000there" ] }, + "tests": [ + { + "description": "match string with nul", + "data": "hello\u0000there", + "valid": true + }, + { + "description": "do not match string lacking nul", + "data": "hellothere", + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/exclusiveMaximum.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/exclusiveMaximum.json new file mode 100644 index 000000000..dc3cd709d --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/exclusiveMaximum.json @@ -0,0 +1,30 @@ +[ + { + "description": "exclusiveMaximum validation", + "schema": { + "exclusiveMaximum": 3.0 + }, + "tests": [ + { + "description": "below the exclusiveMaximum is valid", + "data": 2.2, + "valid": true + }, + { + "description": "boundary point is invalid", + "data": 3.0, + "valid": false + }, + { + "description": "above the exclusiveMaximum is invalid", + "data": 3.5, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/exclusiveMinimum.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/exclusiveMinimum.json new file mode 100644 index 000000000..b38d7ecec --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/exclusiveMinimum.json @@ -0,0 +1,30 @@ +[ + { + "description": "exclusiveMinimum validation", + "schema": { + "exclusiveMinimum": 1.1 + }, + "tests": [ + { + "description": "above the exclusiveMinimum is valid", + "data": 1.2, + "valid": true + }, + { + "description": "boundary point is invalid", + "data": 1.1, + "valid": false + }, + { + "description": "below the exclusiveMinimum is invalid", + "data": 0.6, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/format.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/format.json new file mode 100644 index 000000000..e2447d60f --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/format.json @@ -0,0 +1,614 @@ +[ + { + "description": "email format", + "schema": { "format": "email" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "idn-email format", + "schema": { "format": "idn-email" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "regex format", + "schema": { "format": "regex" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "ipv4 format", + "schema": { "format": "ipv4" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "ipv6 format", + "schema": { "format": "ipv6" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "idn-hostname format", + "schema": { "format": "idn-hostname" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "hostname format", + "schema": { "format": "hostname" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "date format", + "schema": { "format": "date" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "date-time format", + "schema": { "format": "date-time" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "time format", + "schema": { "format": "time" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "json-pointer format", + "schema": { "format": "json-pointer" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "relative-json-pointer format", + "schema": { "format": "relative-json-pointer" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "iri format", + "schema": { "format": "iri" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "iri-reference format", + "schema": { "format": "iri-reference" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "uri format", + "schema": { "format": "uri" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "uri-reference format", + "schema": { "format": "uri-reference" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "uri-template format", + "schema": { "format": "uri-template" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/id.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/id.json new file mode 100644 index 000000000..b58e0d007 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/id.json @@ -0,0 +1,53 @@ +[ + { + "description": "id inside an enum is not a real identifier", + "comment": "the implementation must not be confused by an id buried in the enum", + "schema": { + "definitions": { + "id_in_enum": { + "enum": [ + { + "$id": "https://localhost:1234/id/my_identifier.json", + "type": "null" + } + ] + }, + "real_id_in_schema": { + "$id": "https://localhost:1234/id/my_identifier.json", + "type": "string" + }, + "zzz_id_in_const": { + "const": { + "$id": "https://localhost:1234/id/my_identifier.json", + "type": "null" + } + } + }, + "anyOf": [ + { "$ref": "#/definitions/id_in_enum" }, + { "$ref": "https://localhost:1234/id/my_identifier.json" } + ] + }, + "tests": [ + { + "description": "exact match to enum, and type matches", + "data": { + "$id": "https://localhost:1234/id/my_identifier.json", + "type": "null" + }, + "valid": true + }, + { + "description": "match $ref to id", + "data": "a string to match #/definitions/id_in_enum", + "valid": true + }, + { + "description": "no match on enum or $ref to id", + "data": 1, + "valid": false + } + ] + } + +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/if-then-else.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/if-then-else.json new file mode 100644 index 000000000..284e91912 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/if-then-else.json @@ -0,0 +1,258 @@ +[ + { + "description": "ignore if without then or else", + "schema": { + "if": { + "const": 0 + } + }, + "tests": [ + { + "description": "valid when valid against lone if", + "data": 0, + "valid": true + }, + { + "description": "valid when invalid against lone if", + "data": "hello", + "valid": true + } + ] + }, + { + "description": "ignore then without if", + "schema": { + "then": { + "const": 0 + } + }, + "tests": [ + { + "description": "valid when valid against lone then", + "data": 0, + "valid": true + }, + { + "description": "valid when invalid against lone then", + "data": "hello", + "valid": true + } + ] + }, + { + "description": "ignore else without if", + "schema": { + "else": { + "const": 0 + } + }, + "tests": [ + { + "description": "valid when valid against lone else", + "data": 0, + "valid": true + }, + { + "description": "valid when invalid against lone else", + "data": "hello", + "valid": true + } + ] + }, + { + "description": "if and then without else", + "schema": { + "if": { + "exclusiveMaximum": 0 + }, + "then": { + "minimum": -10 + } + }, + "tests": [ + { + "description": "valid through then", + "data": -1, + "valid": true + }, + { + "description": "invalid through then", + "data": -100, + "valid": false + }, + { + "description": "valid when if test fails", + "data": 3, + "valid": true + } + ] + }, + { + "description": "if and else without then", + "schema": { + "if": { + "exclusiveMaximum": 0 + }, + "else": { + "multipleOf": 2 + } + }, + "tests": [ + { + "description": "valid when if test passes", + "data": -1, + "valid": true + }, + { + "description": "valid through else", + "data": 4, + "valid": true + }, + { + "description": "invalid through else", + "data": 3, + "valid": false + } + ] + }, + { + "description": "validate against correct branch, then vs else", + "schema": { + "if": { + "exclusiveMaximum": 0 + }, + "then": { + "minimum": -10 + }, + "else": { + "multipleOf": 2 + } + }, + "tests": [ + { + "description": "valid through then", + "data": -1, + "valid": true + }, + { + "description": "invalid through then", + "data": -100, + "valid": false + }, + { + "description": "valid through else", + "data": 4, + "valid": true + }, + { + "description": "invalid through else", + "data": 3, + "valid": false + } + ] + }, + { + "description": "non-interference across combined schemas", + "schema": { + "allOf": [ + { + "if": { + "exclusiveMaximum": 0 + } + }, + { + "then": { + "minimum": -10 + } + }, + { + "else": { + "multipleOf": 2 + } + } + ] + }, + "tests": [ + { + "description": "valid, but would have been invalid through then", + "data": -100, + "valid": true + }, + { + "description": "valid, but would have been invalid through else", + "data": 3, + "valid": true + } + ] + }, + { + "description": "if with boolean schema true", + "schema": { + "if": true, + "then": { "const": "then" }, + "else": { "const": "else" } + }, + "tests": [ + { + "description": "boolean schema true in if always chooses the then path (valid)", + "data": "then", + "valid": true + }, + { + "description": "boolean schema true in if always chooses the then path (invalid)", + "data": "else", + "valid": false + } + ] + }, + { + "description": "if with boolean schema false", + "schema": { + "if": false, + "then": { "const": "then" }, + "else": { "const": "else" } + }, + "tests": [ + { + "description": "boolean schema false in if always chooses the else path (invalid)", + "data": "then", + "valid": false + }, + { + "description": "boolean schema false in if always chooses the else path (valid)", + "data": "else", + "valid": true + } + ] + }, + { + "description": "if appears at the end when serialized (keyword processing sequence)", + "schema": { + "then": { "const": "yes" }, + "else": { "const": "other" }, + "if": { "maxLength": 4 } + }, + "tests": [ + { + "description": "yes redirects to then and passes", + "data": "yes", + "valid": true + }, + { + "description": "other redirects to else and passes", + "data": "other", + "valid": true + }, + { + "description": "no redirects to then and fails", + "data": "no", + "valid": false + }, + { + "description": "invalid redirects to else and fails", + "data": "invalid", + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/infinite-loop-detection.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/infinite-loop-detection.json new file mode 100644 index 000000000..f98c74fc6 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/infinite-loop-detection.json @@ -0,0 +1,36 @@ +[ + { + "description": "evaluating the same schema location against the same data location twice is not a sign of an infinite loop", + "schema": { + "definitions": { + "int": { "type": "integer" } + }, + "allOf": [ + { + "properties": { + "foo": { + "$ref": "#/definitions/int" + } + } + }, + { + "additionalProperties": { + "$ref": "#/definitions/int" + } + } + ] + }, + "tests": [ + { + "description": "passing case", + "data": { "foo": 1 }, + "valid": true + }, + { + "description": "failing case", + "data": { "foo": "a string" }, + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/items.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/items.json new file mode 100644 index 000000000..67f11840a --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/items.json @@ -0,0 +1,250 @@ +[ + { + "description": "a schema given for items", + "schema": { + "items": {"type": "integer"} + }, + "tests": [ + { + "description": "valid items", + "data": [ 1, 2, 3 ], + "valid": true + }, + { + "description": "wrong type of items", + "data": [1, "x"], + "valid": false + }, + { + "description": "ignores non-arrays", + "data": {"foo" : "bar"}, + "valid": true + }, + { + "description": "JavaScript pseudo-array is valid", + "data": { + "0": "invalid", + "length": 1 + }, + "valid": true + } + ] + }, + { + "description": "an array of schemas for items", + "schema": { + "items": [ + {"type": "integer"}, + {"type": "string"} + ] + }, + "tests": [ + { + "description": "correct types", + "data": [ 1, "foo" ], + "valid": true + }, + { + "description": "wrong types", + "data": [ "foo", 1 ], + "valid": false + }, + { + "description": "incomplete array of items", + "data": [ 1 ], + "valid": true + }, + { + "description": "array with additional items", + "data": [ 1, "foo", true ], + "valid": true + }, + { + "description": "empty array", + "data": [ ], + "valid": true + }, + { + "description": "JavaScript pseudo-array is valid", + "data": { + "0": "invalid", + "1": "valid", + "length": 2 + }, + "valid": true + } + ] + }, + { + "description": "items with boolean schema (true)", + "schema": {"items": true}, + "tests": [ + { + "description": "any array is valid", + "data": [ 1, "foo", true ], + "valid": true + }, + { + "description": "empty array is valid", + "data": [], + "valid": true + } + ] + }, + { + "description": "items with boolean schema (false)", + "schema": {"items": false}, + "tests": [ + { + "description": "any non-empty array is invalid", + "data": [ 1, "foo", true ], + "valid": false + }, + { + "description": "empty array is valid", + "data": [], + "valid": true + } + ] + }, + { + "description": "items with boolean schemas", + "schema": { + "items": [true, false] + }, + "tests": [ + { + "description": "array with one item is valid", + "data": [ 1 ], + "valid": true + }, + { + "description": "array with two items is invalid", + "data": [ 1, "foo" ], + "valid": false + }, + { + "description": "empty array is valid", + "data": [], + "valid": true + } + ] + }, + { + "description": "items and subitems", + "schema": { + "definitions": { + "item": { + "type": "array", + "additionalItems": false, + "items": [ + { "$ref": "#/definitions/sub-item" }, + { "$ref": "#/definitions/sub-item" } + ] + }, + "sub-item": { + "type": "object", + "required": ["foo"] + } + }, + "type": "array", + "additionalItems": false, + "items": [ + { "$ref": "#/definitions/item" }, + { "$ref": "#/definitions/item" }, + { "$ref": "#/definitions/item" } + ] + }, + "tests": [ + { + "description": "valid items", + "data": [ + [ {"foo": null}, {"foo": null} ], + [ {"foo": null}, {"foo": null} ], + [ {"foo": null}, {"foo": null} ] + ], + "valid": true + }, + { + "description": "too many items", + "data": [ + [ {"foo": null}, {"foo": null} ], + [ {"foo": null}, {"foo": null} ], + [ {"foo": null}, {"foo": null} ], + [ {"foo": null}, {"foo": null} ] + ], + "valid": false + }, + { + "description": "too many sub-items", + "data": [ + [ {"foo": null}, {"foo": null}, {"foo": null} ], + [ {"foo": null}, {"foo": null} ], + [ {"foo": null}, {"foo": null} ] + ], + "valid": false + }, + { + "description": "wrong item", + "data": [ + {"foo": null}, + [ {"foo": null}, {"foo": null} ], + [ {"foo": null}, {"foo": null} ] + ], + "valid": false + }, + { + "description": "wrong sub-item", + "data": [ + [ {}, {"foo": null} ], + [ {"foo": null}, {"foo": null} ], + [ {"foo": null}, {"foo": null} ] + ], + "valid": false + }, + { + "description": "fewer items is valid", + "data": [ + [ {"foo": null} ], + [ {"foo": null} ] + ], + "valid": true + } + ] + }, + { + "description": "nested items", + "schema": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "number" + } + } + } + } + }, + "tests": [ + { + "description": "valid nested array", + "data": [[[[1]], [[2],[3]]], [[[4], [5], [6]]]], + "valid": true + }, + { + "description": "nested array with invalid type", + "data": [[[["1"]], [[2],[3]]], [[[4], [5], [6]]]], + "valid": false + }, + { + "description": "not deep enough", + "data": [[[1], [2],[3]], [[4], [5], [6]]], + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/maxItems.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/maxItems.json new file mode 100644 index 000000000..3b53a6b37 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/maxItems.json @@ -0,0 +1,28 @@ +[ + { + "description": "maxItems validation", + "schema": {"maxItems": 2}, + "tests": [ + { + "description": "shorter is valid", + "data": [1], + "valid": true + }, + { + "description": "exact length is valid", + "data": [1, 2], + "valid": true + }, + { + "description": "too long is invalid", + "data": [1, 2, 3], + "valid": false + }, + { + "description": "ignores non-arrays", + "data": "foobar", + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/maxLength.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/maxLength.json new file mode 100644 index 000000000..811d35b25 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/maxLength.json @@ -0,0 +1,33 @@ +[ + { + "description": "maxLength validation", + "schema": {"maxLength": 2}, + "tests": [ + { + "description": "shorter is valid", + "data": "f", + "valid": true + }, + { + "description": "exact length is valid", + "data": "fo", + "valid": true + }, + { + "description": "too long is invalid", + "data": "foo", + "valid": false + }, + { + "description": "ignores non-strings", + "data": 100, + "valid": true + }, + { + "description": "two supplementary Unicode code points is long enough", + "data": "\uD83D\uDCA9\uD83D\uDCA9", + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/maxProperties.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/maxProperties.json new file mode 100644 index 000000000..aa7209f53 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/maxProperties.json @@ -0,0 +1,54 @@ +[ + { + "description": "maxProperties validation", + "schema": {"maxProperties": 2}, + "tests": [ + { + "description": "shorter is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "exact length is valid", + "data": {"foo": 1, "bar": 2}, + "valid": true + }, + { + "description": "too long is invalid", + "data": {"foo": 1, "bar": 2, "baz": 3}, + "valid": false + }, + { + "description": "ignores arrays", + "data": [1, 2, 3], + "valid": true + }, + { + "description": "ignores strings", + "data": "foobar", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + } + ] + }, + { + "description": "maxProperties = 0 means the object is empty", + "schema": { "maxProperties": 0 }, + "tests": [ + { + "description": "no properties is valid", + "data": {}, + "valid": true + }, + { + "description": "one property is invalid", + "data": { "foo": 1 }, + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/maximum.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/maximum.json new file mode 100644 index 000000000..6844a39ee --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/maximum.json @@ -0,0 +1,54 @@ +[ + { + "description": "maximum validation", + "schema": {"maximum": 3.0}, + "tests": [ + { + "description": "below the maximum is valid", + "data": 2.6, + "valid": true + }, + { + "description": "boundary point is valid", + "data": 3.0, + "valid": true + }, + { + "description": "above the maximum is invalid", + "data": 3.5, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true + } + ] + }, + { + "description": "maximum validation with unsigned integer", + "schema": {"maximum": 300}, + "tests": [ + { + "description": "below the maximum is invalid", + "data": 299.97, + "valid": true + }, + { + "description": "boundary point integer is valid", + "data": 300, + "valid": true + }, + { + "description": "boundary point float is valid", + "data": 300.00, + "valid": true + }, + { + "description": "above the maximum is invalid", + "data": 300.5, + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/minItems.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/minItems.json new file mode 100644 index 000000000..ed5118815 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/minItems.json @@ -0,0 +1,28 @@ +[ + { + "description": "minItems validation", + "schema": {"minItems": 1}, + "tests": [ + { + "description": "longer is valid", + "data": [1, 2], + "valid": true + }, + { + "description": "exact length is valid", + "data": [1], + "valid": true + }, + { + "description": "too short is invalid", + "data": [], + "valid": false + }, + { + "description": "ignores non-arrays", + "data": "", + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/minLength.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/minLength.json new file mode 100644 index 000000000..3f09158de --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/minLength.json @@ -0,0 +1,33 @@ +[ + { + "description": "minLength validation", + "schema": {"minLength": 2}, + "tests": [ + { + "description": "longer is valid", + "data": "foo", + "valid": true + }, + { + "description": "exact length is valid", + "data": "fo", + "valid": true + }, + { + "description": "too short is invalid", + "data": "f", + "valid": false + }, + { + "description": "ignores non-strings", + "data": 1, + "valid": true + }, + { + "description": "one supplementary Unicode code point is not long enough", + "data": "\uD83D\uDCA9", + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/minProperties.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/minProperties.json new file mode 100644 index 000000000..49a0726e0 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/minProperties.json @@ -0,0 +1,38 @@ +[ + { + "description": "minProperties validation", + "schema": {"minProperties": 1}, + "tests": [ + { + "description": "longer is valid", + "data": {"foo": 1, "bar": 2}, + "valid": true + }, + { + "description": "exact length is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "too short is invalid", + "data": {}, + "valid": false + }, + { + "description": "ignores arrays", + "data": [], + "valid": true + }, + { + "description": "ignores strings", + "data": "", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/minimum.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/minimum.json new file mode 100644 index 000000000..21ae50e0e --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/minimum.json @@ -0,0 +1,69 @@ +[ + { + "description": "minimum validation", + "schema": {"minimum": 1.1}, + "tests": [ + { + "description": "above the minimum is valid", + "data": 2.6, + "valid": true + }, + { + "description": "boundary point is valid", + "data": 1.1, + "valid": true + }, + { + "description": "below the minimum is invalid", + "data": 0.6, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true + } + ] + }, + { + "description": "minimum validation with signed integer", + "schema": {"minimum": -2}, + "tests": [ + { + "description": "negative above the minimum is valid", + "data": -1, + "valid": true + }, + { + "description": "positive above the minimum is valid", + "data": 0, + "valid": true + }, + { + "description": "boundary point is valid", + "data": -2, + "valid": true + }, + { + "description": "boundary point with float is valid", + "data": -2.0, + "valid": true + }, + { + "description": "float below the minimum is invalid", + "data": -2.0001, + "valid": false + }, + { + "description": "int below the minimum is invalid", + "data": -3, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/multipleOf.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/multipleOf.json new file mode 100644 index 000000000..faa87cff5 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/multipleOf.json @@ -0,0 +1,71 @@ +[ + { + "description": "by int", + "schema": {"multipleOf": 2}, + "tests": [ + { + "description": "int by int", + "data": 10, + "valid": true + }, + { + "description": "int by int fail", + "data": 7, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "by number", + "schema": {"multipleOf": 1.5}, + "tests": [ + { + "description": "zero is multiple of anything", + "data": 0, + "valid": true + }, + { + "description": "4.5 is multiple of 1.5", + "data": 4.5, + "valid": true + }, + { + "description": "35 is not multiple of 1.5", + "data": 35, + "valid": false + } + ] + }, + { + "description": "by small number", + "schema": {"multipleOf": 0.0001}, + "tests": [ + { + "description": "0.0075 is multiple of 0.0001", + "data": 0.0075, + "valid": true + }, + { + "description": "0.00751 is not multiple of 0.0001", + "data": 0.00751, + "valid": false + } + ] + }, + { + "description": "invalid instance should not raise error when float division = inf", + "schema": {"type": "integer", "multipleOf": 0.123456789}, + "tests": [ + { + "description": "always invalid, but naive implementations may raise an overflow error", + "data": 1e308, + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/not.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/not.json new file mode 100644 index 000000000..a48a798aa --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/not.json @@ -0,0 +1,117 @@ +[ + { + "description": "not", + "schema": { + "not": {"type": "integer"} + }, + "tests": [ + { + "description": "allowed", + "data": "foo", + "valid": true + }, + { + "description": "disallowed", + "data": 1, + "valid": false + } + ] + }, + { + "description": "not multiple types", + "schema": { + "not": {"type": ["integer", "boolean"]} + }, + "tests": [ + { + "description": "valid", + "data": "foo", + "valid": true + }, + { + "description": "mismatch", + "data": 1, + "valid": false + }, + { + "description": "other mismatch", + "data": true, + "valid": false + } + ] + }, + { + "description": "not more complex schema", + "schema": { + "not": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + } + }, + "tests": [ + { + "description": "match", + "data": 1, + "valid": true + }, + { + "description": "other match", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "mismatch", + "data": {"foo": "bar"}, + "valid": false + } + ] + }, + { + "description": "forbidden property", + "schema": { + "properties": { + "foo": { + "not": {} + } + } + }, + "tests": [ + { + "description": "property present", + "data": {"foo": 1, "bar": 2}, + "valid": false + }, + { + "description": "property absent", + "data": {"bar": 1, "baz": 2}, + "valid": true + } + ] + }, + { + "description": "not with boolean schema true", + "schema": {"not": true}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "not with boolean schema false", + "schema": {"not": false}, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/oneOf.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/oneOf.json new file mode 100644 index 000000000..eeb7ae866 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/oneOf.json @@ -0,0 +1,274 @@ +[ + { + "description": "oneOf", + "schema": { + "oneOf": [ + { + "type": "integer" + }, + { + "minimum": 2 + } + ] + }, + "tests": [ + { + "description": "first oneOf valid", + "data": 1, + "valid": true + }, + { + "description": "second oneOf valid", + "data": 2.5, + "valid": true + }, + { + "description": "both oneOf valid", + "data": 3, + "valid": false + }, + { + "description": "neither oneOf valid", + "data": 1.5, + "valid": false + } + ] + }, + { + "description": "oneOf with base schema", + "schema": { + "type": "string", + "oneOf" : [ + { + "minLength": 2 + }, + { + "maxLength": 4 + } + ] + }, + "tests": [ + { + "description": "mismatch base schema", + "data": 3, + "valid": false + }, + { + "description": "one oneOf valid", + "data": "foobar", + "valid": true + }, + { + "description": "both oneOf valid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "oneOf with boolean schemas, all true", + "schema": {"oneOf": [true, true, true]}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "oneOf with boolean schemas, one true", + "schema": {"oneOf": [true, false, false]}, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "oneOf with boolean schemas, more than one true", + "schema": {"oneOf": [true, true, false]}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "oneOf with boolean schemas, all false", + "schema": {"oneOf": [false, false, false]}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "oneOf complex types", + "schema": { + "oneOf": [ + { + "properties": { + "bar": {"type": "integer"} + }, + "required": ["bar"] + }, + { + "properties": { + "foo": {"type": "string"} + }, + "required": ["foo"] + } + ] + }, + "tests": [ + { + "description": "first oneOf valid (complex)", + "data": {"bar": 2}, + "valid": true + }, + { + "description": "second oneOf valid (complex)", + "data": {"foo": "baz"}, + "valid": true + }, + { + "description": "both oneOf valid (complex)", + "data": {"foo": "baz", "bar": 2}, + "valid": false + }, + { + "description": "neither oneOf valid (complex)", + "data": {"foo": 2, "bar": "quux"}, + "valid": false + } + ] + }, + { + "description": "oneOf with empty schema", + "schema": { + "oneOf": [ + { "type": "number" }, + {} + ] + }, + "tests": [ + { + "description": "one valid - valid", + "data": "foo", + "valid": true + }, + { + "description": "both valid - invalid", + "data": 123, + "valid": false + } + ] + }, + { + "description": "oneOf with required", + "schema": { + "type": "object", + "oneOf": [ + { "required": ["foo", "bar"] }, + { "required": ["foo", "baz"] } + ] + }, + "tests": [ + { + "description": "both invalid - invalid", + "data": {"bar": 2}, + "valid": false + }, + { + "description": "first valid - valid", + "data": {"foo": 1, "bar": 2}, + "valid": true + }, + { + "description": "second valid - valid", + "data": {"foo": 1, "baz": 3}, + "valid": true + }, + { + "description": "both valid - invalid", + "data": {"foo": 1, "bar": 2, "baz" : 3}, + "valid": false + } + ] + }, + { + "description": "oneOf with missing optional property", + "schema": { + "oneOf": [ + { + "properties": { + "bar": true, + "baz": true + }, + "required": ["bar"] + }, + { + "properties": { + "foo": true + }, + "required": ["foo"] + } + ] + }, + "tests": [ + { + "description": "first oneOf valid", + "data": {"bar": 8}, + "valid": true + }, + { + "description": "second oneOf valid", + "data": {"foo": "foo"}, + "valid": true + }, + { + "description": "both oneOf valid", + "data": {"foo": "foo", "bar": 8}, + "valid": false + }, + { + "description": "neither oneOf valid", + "data": {"baz": "quux"}, + "valid": false + } + ] + }, + { + "description": "nested oneOf, to check validation semantics", + "schema": { + "oneOf": [ + { + "oneOf": [ + { + "type": "null" + } + ] + } + ] + }, + "tests": [ + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "anything non-null is invalid", + "data": 123, + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/bignum.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/bignum.json new file mode 100644 index 000000000..3f49226ac --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/bignum.json @@ -0,0 +1,93 @@ +[ + { + "description": "integer", + "schema": { "type": "integer" }, + "tests": [ + { + "description": "a bignum is an integer", + "data": 12345678910111213141516171819202122232425262728293031, + "valid": true + }, + { + "description": "a negative bignum is an integer", + "data": -12345678910111213141516171819202122232425262728293031, + "valid": true + } + ] + }, + { + "description": "number", + "schema": { "type": "number" }, + "tests": [ + { + "description": "a bignum is a number", + "data": 98249283749234923498293171823948729348710298301928331, + "valid": true + }, + { + "description": "a negative bignum is a number", + "data": -98249283749234923498293171823948729348710298301928331, + "valid": true + } + ] + }, + { + "description": "string", + "schema": { "type": "string" }, + "tests": [ + { + "description": "a bignum is not a string", + "data": 98249283749234923498293171823948729348710298301928331, + "valid": false + } + ] + }, + { + "description": "integer comparison", + "schema": { "maximum": 18446744073709551615 }, + "tests": [ + { + "description": "comparison works for high numbers", + "data": 18446744073709551600, + "valid": true + } + ] + }, + { + "description": "float comparison with high precision", + "schema": { + "exclusiveMaximum": 972783798187987123879878123.18878137 + }, + "tests": [ + { + "description": "comparison works for high numbers", + "data": 972783798187987123879878123.188781371, + "valid": false + } + ] + }, + { + "description": "integer comparison", + "schema": { "minimum": -18446744073709551615 }, + "tests": [ + { + "description": "comparison works for very negative numbers", + "data": -18446744073709551600, + "valid": true + } + ] + }, + { + "description": "float comparison with high precision on negative numbers", + "schema": { + "exclusiveMinimum": -972783798187987123879878123.18878137 + }, + "tests": [ + { + "description": "comparison works for very negative numbers", + "data": -972783798187987123879878123.188781371, + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/content.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/content.json new file mode 100644 index 000000000..3f5a7430b --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/content.json @@ -0,0 +1,77 @@ +[ + { + "description": "validation of string-encoded content based on media type", + "schema": { + "contentMediaType": "application/json" + }, + "tests": [ + { + "description": "a valid JSON document", + "data": "{\"foo\": \"bar\"}", + "valid": true + }, + { + "description": "an invalid JSON document", + "data": "{:}", + "valid": false + }, + { + "description": "ignores non-strings", + "data": 100, + "valid": true + } + ] + }, + { + "description": "validation of binary string-encoding", + "schema": { + "contentEncoding": "base64" + }, + "tests": [ + { + "description": "a valid base64 string", + "data": "eyJmb28iOiAiYmFyIn0K", + "valid": true + }, + { + "description": "an invalid base64 string (% is not a valid character)", + "data": "eyJmb28iOi%iYmFyIn0K", + "valid": false + }, + { + "description": "ignores non-strings", + "data": 100, + "valid": true + } + ] + }, + { + "description": "validation of binary-encoded media type documents", + "schema": { + "contentMediaType": "application/json", + "contentEncoding": "base64" + }, + "tests": [ + { + "description": "a valid base64-encoded JSON document", + "data": "eyJmb28iOiAiYmFyIn0K", + "valid": true + }, + { + "description": "a validly-encoded invalid JSON document", + "data": "ezp9Cg==", + "valid": false + }, + { + "description": "an invalid base64 string that is valid JSON", + "data": "{}", + "valid": false + }, + { + "description": "ignores non-strings", + "data": 100, + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/ecmascript-regex.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/ecmascript-regex.json new file mode 100644 index 000000000..fb02e99f7 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/ecmascript-regex.json @@ -0,0 +1,292 @@ +[ + { + "description": "ECMA 262 regex $ does not match trailing newline", + "schema": { + "type": "string", + "pattern": "^abc$" + }, + "tests": [ + { + "description": "matches in Python, but should not in jsonschema", + "data": "abc\\n", + "valid": false + }, + { + "description": "should match", + "data": "abc", + "valid": true + } + ] + }, + { + "description": "ECMA 262 regex converts \\t to horizontal tab", + "schema": { + "type": "string", + "pattern": "^\\t$" + }, + "tests": [ + { + "description": "does not match", + "data": "\\t", + "valid": false + }, + { + "description": "matches", + "data": "\u0009", + "valid": true + } + ] + }, + { + "description": "ECMA 262 regex escapes control codes with \\c and upper letter", + "schema": { + "type": "string", + "pattern": "^\\cC$" + }, + "tests": [ + { + "description": "does not match", + "data": "\\cC", + "valid": false + }, + { + "description": "matches", + "data": "\u0003", + "valid": true + } + ] + }, + { + "description": "ECMA 262 regex escapes control codes with \\c and lower letter", + "schema": { + "type": "string", + "pattern": "^\\cc$" + }, + "tests": [ + { + "description": "does not match", + "data": "\\cc", + "valid": false + }, + { + "description": "matches", + "data": "\u0003", + "valid": true + } + ] + }, + { + "description": "ECMA 262 \\d matches ascii digits only", + "schema": { + "type": "string", + "pattern": "^\\d$" + }, + "tests": [ + { + "description": "ASCII zero matches", + "data": "0", + "valid": true + }, + { + "description": "NKO DIGIT ZERO does not match (unlike e.g. Python)", + "data": "߀", + "valid": false + }, + { + "description": "NKO DIGIT ZERO (as \\u escape) does not match", + "data": "\u07c0", + "valid": false + } + ] + }, + { + "description": "ECMA 262 \\D matches everything but ascii digits", + "schema": { + "type": "string", + "pattern": "^\\D$" + }, + "tests": [ + { + "description": "ASCII zero does not match", + "data": "0", + "valid": false + }, + { + "description": "NKO DIGIT ZERO matches (unlike e.g. Python)", + "data": "߀", + "valid": true + }, + { + "description": "NKO DIGIT ZERO (as \\u escape) matches", + "data": "\u07c0", + "valid": true + } + ] + }, + { + "description": "ECMA 262 \\w matches ascii letters only", + "schema": { + "type": "string", + "pattern": "^\\w$" + }, + "tests": [ + { + "description": "ASCII 'a' matches", + "data": "a", + "valid": true + }, + { + "description": "latin-1 e-acute does not match (unlike e.g. Python)", + "data": "é", + "valid": false + } + ] + }, + { + "description": "ECMA 262 \\W matches everything but ascii letters", + "schema": { + "type": "string", + "pattern": "^\\W$" + }, + "tests": [ + { + "description": "ASCII 'a' does not match", + "data": "a", + "valid": false + }, + { + "description": "latin-1 e-acute matches (unlike e.g. Python)", + "data": "é", + "valid": true + } + ] + }, + { + "description": "ECMA 262 \\s matches whitespace", + "schema": { + "type": "string", + "pattern": "^\\s$" + }, + "tests": [ + { + "description": "ASCII space matches", + "data": " ", + "valid": true + }, + { + "description": "Character tabulation matches", + "data": "\t", + "valid": true + }, + { + "description": "Line tabulation matches", + "data": "\u000b", + "valid": true + }, + { + "description": "Form feed matches", + "data": "\u000c", + "valid": true + }, + { + "description": "latin-1 non-breaking-space matches", + "data": "\u00a0", + "valid": true + }, + { + "description": "zero-width whitespace matches", + "data": "\ufeff", + "valid": true + }, + { + "description": "line feed matches (line terminator)", + "data": "\u000a", + "valid": true + }, + { + "description": "paragraph separator matches (line terminator)", + "data": "\u2029", + "valid": true + }, + { + "description": "EM SPACE matches (Space_Separator)", + "data": "\u2003", + "valid": true + }, + { + "description": "Non-whitespace control does not match", + "data": "\u0001", + "valid": false + }, + { + "description": "Non-whitespace does not match", + "data": "\u2013", + "valid": false + } + ] + }, + { + "description": "ECMA 262 \\S matches everything but whitespace", + "schema": { + "type": "string", + "pattern": "^\\S$" + }, + "tests": [ + { + "description": "ASCII space does not match", + "data": " ", + "valid": false + }, + { + "description": "Character tabulation does not match", + "data": "\t", + "valid": false + }, + { + "description": "Line tabulation does not match", + "data": "\u000b", + "valid": false + }, + { + "description": "Form feed does not match", + "data": "\u000c", + "valid": false + }, + { + "description": "latin-1 non-breaking-space does not match", + "data": "\u00a0", + "valid": false + }, + { + "description": "zero-width whitespace does not match", + "data": "\ufeff", + "valid": false + }, + { + "description": "line feed does not match (line terminator)", + "data": "\u000a", + "valid": false + }, + { + "description": "paragraph separator does not match (line terminator)", + "data": "\u2029", + "valid": false + }, + { + "description": "EM SPACE does not match (Space_Separator)", + "data": "\u2003", + "valid": false + }, + { + "description": "Non-whitespace control matches", + "data": "\u0001", + "valid": true + }, + { + "description": "Non-whitespace matches", + "data": "\u2013", + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/float-overflow.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/float-overflow.json new file mode 100644 index 000000000..52ff9827c --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/float-overflow.json @@ -0,0 +1,13 @@ +[ + { + "description": "all integers are multiples of 0.5, if overflow is handled", + "schema": {"type": "integer", "multipleOf": 0.5}, + "tests": [ + { + "description": "valid if optional overflow handling is implemented", + "data": 1e308, + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/date-time.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/date-time.json new file mode 100644 index 000000000..5f911efe4 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/date-time.json @@ -0,0 +1,73 @@ +[ + { + "description": "validation of date-time strings", + "schema": {"format": "date-time"}, + "tests": [ + { + "description": "a valid date-time string", + "data": "1963-06-19T08:30:06.283185Z", + "valid": true + }, + { + "description": "a valid date-time string without second fraction", + "data": "1963-06-19T08:30:06Z", + "valid": true + }, + { + "description": "a valid date-time string with plus offset", + "data": "1937-01-01T12:00:27.87+00:20", + "valid": true + }, + { + "description": "a valid date-time string with minus offset", + "data": "1990-12-31T15:59:50.123-08:00", + "valid": true + }, + { + "description": "a invalid day in date-time string", + "data": "1990-02-31T15:59:60.123-08:00", + "valid": false + }, + { + "description": "an invalid offset in date-time string", + "data": "1990-12-31T15:59:60-24:00", + "valid": false + }, + { + "description": "an invalid date-time string", + "data": "06/19/1963 08:30:06 PST", + "valid": false + }, + { + "description": "case-insensitive T and Z", + "data": "1963-06-19t08:30:06.283185z", + "valid": true + }, + { + "description": "only RFC3339 not all of ISO 8601 are valid", + "data": "2013-350T01:01:01", + "valid": false + }, + { + "description": "invalid non-padded month dates", + "data": "1963-6-19T08:30:06.283185Z", + "valid": false + }, + { + "description": "invalid non-padded day dates", + "data": "1963-06-1T08:30:06.283185Z", + "valid": false + }, + { + "description": "non-ascii digits should be rejected in the date portion", + "data": "1963-06-1৪T00:00:00Z", + "valid": false + }, + { + "description": "non-ascii digits should be rejected in the time portion", + "data": "1963-06-11T0৪:00:00Z", + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/date.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/date.json new file mode 100644 index 000000000..6cc2feb8c --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/date.json @@ -0,0 +1,193 @@ +[ + { + "description": "validation of date strings", + "schema": {"format": "date"}, + "tests": [ + { + "description": "a valid date string", + "data": "1963-06-19", + "valid": true + }, + { + "description": "a valid date string with 31 days in January", + "data": "2020-01-31", + "valid": true + }, + { + "description": "a invalid date string with 32 days in January", + "data": "2020-01-32", + "valid": false + }, + { + "description": "a valid date string with 28 days in February (normal)", + "data": "2021-02-28", + "valid": true + }, + { + "description": "a invalid date string with 29 days in February (normal)", + "data": "2021-02-29", + "valid": false + }, + { + "description": "a valid date string with 29 days in February (leap)", + "data": "2020-02-29", + "valid": true + }, + { + "description": "a invalid date string with 30 days in February (leap)", + "data": "2020-02-30", + "valid": false + }, + { + "description": "a valid date string with 31 days in March", + "data": "2020-03-31", + "valid": true + }, + { + "description": "a invalid date string with 32 days in March", + "data": "2020-03-32", + "valid": false + }, + { + "description": "a valid date string with 30 days in April", + "data": "2020-04-30", + "valid": true + }, + { + "description": "a invalid date string with 31 days in April", + "data": "2020-04-31", + "valid": false + }, + { + "description": "a valid date string with 31 days in May", + "data": "2020-05-31", + "valid": true + }, + { + "description": "a invalid date string with 32 days in May", + "data": "2020-05-32", + "valid": false + }, + { + "description": "a valid date string with 30 days in June", + "data": "2020-06-30", + "valid": true + }, + { + "description": "a invalid date string with 31 days in June", + "data": "2020-06-31", + "valid": false + }, + { + "description": "a valid date string with 31 days in July", + "data": "2020-07-31", + "valid": true + }, + { + "description": "a invalid date string with 32 days in July", + "data": "2020-07-32", + "valid": false + }, + { + "description": "a valid date string with 31 days in August", + "data": "2020-08-31", + "valid": true + }, + { + "description": "a invalid date string with 32 days in August", + "data": "2020-08-32", + "valid": false + }, + { + "description": "a valid date string with 30 days in September", + "data": "2020-09-30", + "valid": true + }, + { + "description": "a invalid date string with 31 days in September", + "data": "2020-09-31", + "valid": false + }, + { + "description": "a valid date string with 31 days in October", + "data": "2020-10-31", + "valid": true + }, + { + "description": "a invalid date string with 32 days in October", + "data": "2020-10-32", + "valid": false + }, + { + "description": "a valid date string with 30 days in November", + "data": "2020-11-30", + "valid": true + }, + { + "description": "a invalid date string with 31 days in November", + "data": "2020-11-31", + "valid": false + }, + { + "description": "a valid date string with 31 days in December", + "data": "2020-12-31", + "valid": true + }, + { + "description": "a invalid date string with 32 days in December", + "data": "2020-12-32", + "valid": false + }, + { + "description": "a invalid date string with invalid month", + "data": "2020-13-01", + "valid": false + }, + { + "description": "an invalid date string", + "data": "06/19/1963", + "valid": false + }, + { + "description": "only RFC3339 not all of ISO 8601 are valid", + "data": "2013-350", + "valid": false + }, + { + "description": "non-padded month dates are not valid", + "data": "1998-1-20", + "valid": false + }, + { + "description": "non-padded day dates are not valid", + "data": "1998-01-1", + "valid": false + }, + { + "description": "invalid month", + "data": "1998-13-01", + "valid": false + }, + { + "description": "invalid month-day combination", + "data": "1998-04-31", + "valid": false + }, + { + "description": "2021 is not a leap year", + "data": "2021-02-29", + "valid": false + }, + { + "description": "2020 is a leap year", + "data": "2020-02-29", + "valid": true + }, + { + "description": "non-ascii digits should be rejected", + "data": "1963-06-1৪", + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/email.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/email.json new file mode 100644 index 000000000..02396d269 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/email.json @@ -0,0 +1,53 @@ +[ + { + "description": "validation of e-mail addresses", + "schema": {"format": "email"}, + "tests": [ + { + "description": "a valid e-mail address", + "data": "joe.bloggs@example.com", + "valid": true + }, + { + "description": "an invalid e-mail address", + "data": "2962", + "valid": false + }, + { + "description": "tilde in local part is valid", + "data": "te~st@example.com", + "valid": true + }, + { + "description": "tilde before local part is valid", + "data": "~test@example.com", + "valid": true + }, + { + "description": "tilde after local part is valid", + "data": "test~@example.com", + "valid": true + }, + { + "description": "dot before local part is not valid", + "data": ".test@example.com", + "valid": false + }, + { + "description": "dot after local part is not valid", + "data": "test.@example.com", + "valid": false + }, + { + "description": "two separated dots inside local part are valid", + "data": "te.s.t@example.com", + "valid": true + }, + { + "description": "two subsequent dots inside local part are not valid", + "data": "te..st@example.com", + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/hostname.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/hostname.json new file mode 100644 index 000000000..476541a83 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/hostname.json @@ -0,0 +1,68 @@ +[ + { + "description": "validation of host names", + "schema": {"format": "hostname"}, + "tests": [ + { + "description": "a valid host name", + "data": "www.example.com", + "valid": true + }, + { + "description": "a valid punycoded IDN hostname", + "data": "xn--4gbwdl.xn--wgbh1c", + "valid": true + }, + { + "description": "a host name starting with an illegal character", + "data": "-a-host-name-that-starts-with--", + "valid": false + }, + { + "description": "a host name containing illegal characters", + "data": "not_a_valid_host_name", + "valid": false + }, + { + "description": "a host name with a component too long", + "data": "a-vvvvvvvvvvvvvvvveeeeeeeeeeeeeeeerrrrrrrrrrrrrrrryyyyyyyyyyyyyyyy-long-host-name-component", + "valid": false + }, + { + "description": "starts with hyphen", + "data": "-hostname", + "valid": false + }, + { + "description": "ends with hyphen", + "data": "hostname-", + "valid": false + }, + { + "description": "starts with underscore", + "data": "_hostname", + "valid": false + }, + { + "description": "ends with underscore", + "data": "hostname_", + "valid": false + }, + { + "description": "contains underscore", + "data": "host_name", + "valid": false + }, + { + "description": "maximum label length", + "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.com", + "valid": true + }, + { + "description": "exceeds maximum label length", + "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/idn-email.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/idn-email.json new file mode 100644 index 000000000..552d10673 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/idn-email.json @@ -0,0 +1,28 @@ +[ + { + "description": "validation of an internationalized e-mail addresses", + "schema": {"format": "idn-email"}, + "tests": [ + { + "description": "a valid idn e-mail (example@example.test in Hangul)", + "data": "실례@실례.테스트", + "valid": true + }, + { + "description": "an invalid idn e-mail address", + "data": "2962", + "valid": false + }, + { + "description": "a valid e-mail address", + "data": "joe.bloggs@example.com", + "valid": true + }, + { + "description": "an invalid e-mail address", + "data": "2962", + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/idn-hostname.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/idn-hostname.json new file mode 100644 index 000000000..7f10bd83e --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/idn-hostname.json @@ -0,0 +1,274 @@ +[ + { + "description": "validation of internationalized host names", + "schema": {"format": "idn-hostname"}, + "tests": [ + { + "description": "a valid host name (example.test in Hangul)", + "data": "실례.테스트", + "valid": true + }, + { + "description": "illegal first char U+302E Hangul single dot tone mark", + "data": "〮실례.테스트", + "valid": false + }, + { + "description": "contains illegal char U+302E Hangul single dot tone mark", + "data": "실〮례.테스트", + "valid": false + }, + { + "description": "a host name with a component too long", + "data": "실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실실례례테스트례례례례례례례례례례례례례례례례례테스트례례례례례례례례례례례례례례례례례례례테스트례례례례례례례례례례례례테스트례례실례.테스트", + "valid": false + }, + { + "description": "invalid label, correct Punycode", + "comment": "https://tools.ietf.org/html/rfc5890#section-2.3.2.1 https://tools.ietf.org/html/rfc5891#section-4.4 https://tools.ietf.org/html/rfc3492#section-7.1", + "data": "-> $1.00 <--", + "valid": false + }, + { + "description": "valid Chinese Punycode", + "comment": "https://tools.ietf.org/html/rfc5890#section-2.3.2.1 https://tools.ietf.org/html/rfc5891#section-4.4", + "data": "xn--ihqwcrb4cv8a8dqg056pqjye", + "valid": true + }, + { + "description": "invalid Punycode", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.4 https://tools.ietf.org/html/rfc5890#section-2.3.2.1", + "data": "xn--X", + "valid": false + }, + { + "description": "U-label contains \"--\" in the 3rd and 4th position", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.1 https://tools.ietf.org/html/rfc5890#section-2.3.2.1", + "data": "XN--aa---o47jg78q", + "valid": false + }, + { + "description": "U-label starts with a dash", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.1", + "data": "-hello", + "valid": false + }, + { + "description": "U-label ends with a dash", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.1", + "data": "hello-", + "valid": false + }, + { + "description": "U-label starts and ends with a dash", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.1", + "data": "-hello-", + "valid": false + }, + { + "description": "Begins with a Spacing Combining Mark", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.2", + "data": "\u0903hello", + "valid": false + }, + { + "description": "Begins with a Nonspacing Mark", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.2", + "data": "\u0300hello", + "valid": false + }, + { + "description": "Begins with an Enclosing Mark", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.2", + "data": "\u0488hello", + "valid": false + }, + { + "description": "Exceptions that are PVALID, left-to-right chars", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.2 https://tools.ietf.org/html/rfc5892#section-2.6", + "data": "\u00df\u03c2\u0f0b\u3007", + "valid": true + }, + { + "description": "Exceptions that are PVALID, right-to-left chars", + "comment": "https://tools.ietf.org/html/rfc/rfc5891#section-4.2.2 https://tools.ietf.org/html/rfc5892#section-2.6", + "data": "\u06fd\u06fe", + "valid": true + }, + { + "description": "Exceptions that are DISALLOWED, right-to-left chars", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.2 https://tools.ietf.org/html/rfc5892#section-2.6", + "data": "\u0640\u07fa", + "valid": false + }, + { + "description": "Exceptions that are DISALLOWED, left-to-right chars", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.2 https://tools.ietf.org/html/rfc5892#section-2.6 Note: The two combining marks (U+302E and U+302F) are in the middle and not at the start", + "data": "\u3031\u3032\u3033\u3034\u3035\u302e\u302f\u303b", + "valid": false + }, + { + "description": "MIDDLE DOT with no preceding 'l'", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "a\u00b7l", + "valid": false + }, + { + "description": "MIDDLE DOT with nothing preceding", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "\u00b7l", + "valid": false + }, + { + "description": "MIDDLE DOT with no following 'l'", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "l\u00b7a", + "valid": false + }, + { + "description": "MIDDLE DOT with nothing following", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "l\u00b7", + "valid": false + }, + { + "description": "MIDDLE DOT with surrounding 'l's", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.3", + "data": "l\u00b7l", + "valid": true + }, + { + "description": "Greek KERAIA not followed by Greek", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.4", + "data": "\u03b1\u0375S", + "valid": false + }, + { + "description": "Greek KERAIA not followed by anything", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.4", + "data": "\u03b1\u0375", + "valid": false + }, + { + "description": "Greek KERAIA followed by Greek", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.4", + "data": "\u03b1\u0375\u03b2", + "valid": true + }, + { + "description": "Hebrew GERESH not preceded by Hebrew", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.5", + "data": "A\u05f3\u05d1", + "valid": false + }, + { + "description": "Hebrew GERESH not preceded by anything", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.5", + "data": "\u05f3\u05d1", + "valid": false + }, + { + "description": "Hebrew GERESH preceded by Hebrew", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.5", + "data": "\u05d0\u05f3\u05d1", + "valid": true + }, + { + "description": "Hebrew GERSHAYIM not preceded by Hebrew", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.6", + "data": "A\u05f4\u05d1", + "valid": false + }, + { + "description": "Hebrew GERSHAYIM not preceded by anything", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.6", + "data": "\u05f4\u05d1", + "valid": false + }, + { + "description": "Hebrew GERSHAYIM preceded by Hebrew", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.6", + "data": "\u05d0\u05f4\u05d1", + "valid": true + }, + { + "description": "KATAKANA MIDDLE DOT with no Hiragana, Katakana, or Han", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "def\u30fbabc", + "valid": false + }, + { + "description": "KATAKANA MIDDLE DOT with no other characters", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "\u30fb", + "valid": false + }, + { + "description": "KATAKANA MIDDLE DOT with Hiragana", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "\u30fb\u3041", + "valid": true + }, + { + "description": "KATAKANA MIDDLE DOT with Katakana", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "\u30fb\u30a1", + "valid": true + }, + { + "description": "KATAKANA MIDDLE DOT with Han", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.7", + "data": "\u30fb\u4e08", + "valid": true + }, + { + "description": "Arabic-Indic digits mixed with Extended Arabic-Indic digits", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8", + "data": "\u0660\u06f0", + "valid": false + }, + { + "description": "Arabic-Indic digits not mixed with Extended Arabic-Indic digits", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.8", + "data": "\u0628\u0660\u0628", + "valid": true + }, + { + "description": "Extended Arabic-Indic digits not mixed with Arabic-Indic digits", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.9", + "data": "\u06f00", + "valid": true + }, + { + "description": "ZERO WIDTH JOINER not preceded by Virama", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.2 https://www.unicode.org/review/pr-37.pdf", + "data": "\u0915\u200d\u0937", + "valid": false + }, + { + "description": "ZERO WIDTH JOINER not preceded by anything", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.2 https://www.unicode.org/review/pr-37.pdf", + "data": "\u200d\u0937", + "valid": false + }, + { + "description": "ZERO WIDTH JOINER preceded by Virama", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.2 https://www.unicode.org/review/pr-37.pdf", + "data": "\u0915\u094d\u200d\u0937", + "valid": true + }, + { + "description": "ZERO WIDTH NON-JOINER preceded by Virama", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1", + "data": "\u0915\u094d\u200c\u0937", + "valid": true + }, + { + "description": "ZERO WIDTH NON-JOINER not preceded by Virama but matches regexp", + "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1 https://www.w3.org/TR/alreq/#h_disjoining_enforcement", + "data": "\u0628\u064a\u200c\u0628\u064a", + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/ipv4.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/ipv4.json new file mode 100644 index 000000000..4d1092770 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/ipv4.json @@ -0,0 +1,54 @@ +[ + { + "description": "validation of IP addresses", + "schema": {"format": "ipv4"}, + "tests": [ + { + "description": "a valid IP address", + "data": "192.168.0.1", + "valid": true + }, + { + "description": "an IP address with too many components", + "data": "127.0.0.0.1", + "valid": false + }, + { + "description": "an IP address with out-of-range values", + "data": "256.256.256.256", + "valid": false + }, + { + "description": "an IP address without 4 components", + "data": "127.0", + "valid": false + }, + { + "description": "an IP address as an integer", + "data": "0x7f000001", + "valid": false + }, + { + "description": "an IP address as an integer (decimal)", + "data": "2130706433", + "valid": false + }, + { + "description": "leading zeroes should be rejected, as they are treated as octals", + "comment": "see https://sick.codes/universal-netmask-npm-package-used-by-270000-projects-vulnerable-to-octal-input-data-server-side-request-forgery-remote-file-inclusion-local-file-inclusion-and-more-cve-2021-28918/", + "data": "087.10.0.1", + "valid": false + }, + { + "description": "value without leading zero is valid", + "data": "87.10.0.1", + "valid": true + }, + { + "description": "non-ascii digits should be rejected", + "data": "1২7.0.0.1", + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/ipv6.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/ipv6.json new file mode 100644 index 000000000..cf629c60a --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/ipv6.json @@ -0,0 +1,163 @@ +[ + { + "description": "validation of IPv6 addresses", + "schema": {"format": "ipv6"}, + "tests": [ + { + "description": "a valid IPv6 address", + "data": "::1", + "valid": true + }, + { + "description": "an IPv6 address with out-of-range values", + "data": "12345::", + "valid": false + }, + { + "description": "an IPv6 address with too many components", + "data": "1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1", + "valid": false + }, + { + "description": "an IPv6 address containing illegal characters", + "data": "::laptop", + "valid": false + }, + { + "description": "no digits is valid", + "data": "::", + "valid": true + }, + { + "description": "leading colons is valid", + "data": "::42:ff:1", + "valid": true + }, + { + "description": "trailing colons is valid", + "data": "d6::", + "valid": true + }, + { + "description": "missing leading octet is invalid", + "data": ":2:3:4:5:6:7:8", + "valid": false + }, + { + "description": "missing trailing octet is invalid", + "data": "1:2:3:4:5:6:7:", + "valid": false + }, + { + "description": "missing leading octet with omitted octets later", + "data": ":2:3:4::8", + "valid": false + }, + { + "description": "two sets of double colons is invalid", + "data": "1::d6::42", + "valid": false + }, + { + "description": "mixed format with the ipv4 section as decimal octets", + "data": "1::d6:192.168.0.1", + "valid": true + }, + { + "description": "mixed format with double colons between the sections", + "data": "1:2::192.168.0.1", + "valid": true + }, + { + "description": "mixed format with ipv4 section with octet out of range", + "data": "1::2:192.168.256.1", + "valid": false + }, + { + "description": "mixed format with ipv4 section with a hex octet", + "data": "1::2:192.168.ff.1", + "valid": false + }, + { + "description": "mixed format with leading double colons (ipv4-mapped ipv6 address)", + "data": "::ffff:192.168.0.1", + "valid": true + }, + { + "description": "triple colons is invalid", + "data": "1:2:3:4:5:::8", + "valid": false + }, + { + "description": "8 octets", + "data": "1:2:3:4:5:6:7:8", + "valid": true + }, + { + "description": "insufficient octets without double colons", + "data": "1:2:3:4:5:6:7", + "valid": false + }, + { + "description": "no colons is invalid", + "data": "1", + "valid": false + }, + { + "description": "ipv4 is not ipv6", + "data": "127.0.0.1", + "valid": false + }, + { + "description": "ipv4 segment must have 4 octets", + "data": "1:2:3:4:1.2.3", + "valid": false + }, + { + "description": "leading whitespace is invalid", + "data": " ::1", + "valid": false + }, + { + "description": "trailing whitespace is invalid", + "data": "::1 ", + "valid": false + }, + { + "description": "netmask is not a part of ipv6 address", + "data": "fe80::/64", + "valid": false + }, + { + "description": "zone id is not a part of ipv6 address", + "data": "fe80::a%eth1", + "valid": false + }, + { + "description": "a long valid ipv6", + "data": "1000:1000:1000:1000:1000:1000:255.255.255.255", + "valid": true + }, + { + "description": "a long invalid ipv6, below length limit, first", + "data": "100:100:100:100:100:100:255.255.255.255.255", + "valid": false + }, + { + "description": "a long invalid ipv6, below length limit, second", + "data": "100:100:100:100:100:100:100:255.255.255.255", + "valid": false + }, + { + "description": "non-ascii digits should be rejected", + "data": "1:2:3:4:5:6:7:৪", + "valid": false + }, + { + "description": "non-ascii digits should be rejected in the ipv4 portion also", + "data": "1:2::192.16৪.0.1", + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/iri-reference.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/iri-reference.json new file mode 100644 index 000000000..1fd779c23 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/iri-reference.json @@ -0,0 +1,43 @@ +[ + { + "description": "validation of IRI References", + "schema": {"format": "iri-reference"}, + "tests": [ + { + "description": "a valid IRI", + "data": "http://ƒøø.ßår/?∂éœ=πîx#πîüx", + "valid": true + }, + { + "description": "a valid protocol-relative IRI Reference", + "data": "//ƒøø.ßår/?∂éœ=πîx#πîüx", + "valid": true + }, + { + "description": "a valid relative IRI Reference", + "data": "/âππ", + "valid": true + }, + { + "description": "an invalid IRI Reference", + "data": "\\\\WINDOWS\\filëßåré", + "valid": false + }, + { + "description": "a valid IRI Reference", + "data": "âππ", + "valid": true + }, + { + "description": "a valid IRI fragment", + "data": "#ƒrägmênt", + "valid": true + }, + { + "description": "an invalid IRI fragment", + "data": "#ƒräg\\mênt", + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/iri.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/iri.json new file mode 100644 index 000000000..1414f2e6e --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/iri.json @@ -0,0 +1,53 @@ +[ + { + "description": "validation of IRIs", + "schema": {"format": "iri"}, + "tests": [ + { + "description": "a valid IRI with anchor tag", + "data": "http://ƒøø.ßår/?∂éœ=πîx#πîüx", + "valid": true + }, + { + "description": "a valid IRI with anchor tag and parentheses", + "data": "http://ƒøø.com/blah_(wîkïpédiå)_blah#ßité-1", + "valid": true + }, + { + "description": "a valid IRI with URL-encoded stuff", + "data": "http://ƒøø.ßår/?q=Test%20URL-encoded%20stuff", + "valid": true + }, + { + "description": "a valid IRI with many special characters", + "data": "http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com", + "valid": true + }, + { + "description": "a valid IRI based on IPv6", + "data": "http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", + "valid": true + }, + { + "description": "an invalid IRI based on IPv6", + "data": "http://2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "valid": false + }, + { + "description": "an invalid relative IRI Reference", + "data": "/abc", + "valid": false + }, + { + "description": "an invalid IRI", + "data": "\\\\WINDOWS\\filëßåré", + "valid": false + }, + { + "description": "an invalid IRI though valid IRI reference", + "data": "âππ", + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/json-pointer.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/json-pointer.json new file mode 100644 index 000000000..65c2f064f --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/json-pointer.json @@ -0,0 +1,168 @@ +[ + { + "description": "validation of JSON-pointers (JSON String Representation)", + "schema": {"format": "json-pointer"}, + "tests": [ + { + "description": "a valid JSON-pointer", + "data": "/foo/bar~0/baz~1/%a", + "valid": true + }, + { + "description": "not a valid JSON-pointer (~ not escaped)", + "data": "/foo/bar~", + "valid": false + }, + { + "description": "valid JSON-pointer with empty segment", + "data": "/foo//bar", + "valid": true + }, + { + "description": "valid JSON-pointer with the last empty segment", + "data": "/foo/bar/", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #1", + "data": "", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #2", + "data": "/foo", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #3", + "data": "/foo/0", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #4", + "data": "/", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #5", + "data": "/a~1b", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #6", + "data": "/c%d", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #7", + "data": "/e^f", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #8", + "data": "/g|h", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #9", + "data": "/i\\j", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #10", + "data": "/k\"l", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #11", + "data": "/ ", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #12", + "data": "/m~0n", + "valid": true + }, + { + "description": "valid JSON-pointer used adding to the last array position", + "data": "/foo/-", + "valid": true + }, + { + "description": "valid JSON-pointer (- used as object member name)", + "data": "/foo/-/bar", + "valid": true + }, + { + "description": "valid JSON-pointer (multiple escaped characters)", + "data": "/~1~0~0~1~1", + "valid": true + }, + { + "description": "valid JSON-pointer (escaped with fraction part) #1", + "data": "/~1.1", + "valid": true + }, + { + "description": "valid JSON-pointer (escaped with fraction part) #2", + "data": "/~0.1", + "valid": true + }, + { + "description": "not a valid JSON-pointer (URI Fragment Identifier) #1", + "data": "#", + "valid": false + }, + { + "description": "not a valid JSON-pointer (URI Fragment Identifier) #2", + "data": "#/", + "valid": false + }, + { + "description": "not a valid JSON-pointer (URI Fragment Identifier) #3", + "data": "#a", + "valid": false + }, + { + "description": "not a valid JSON-pointer (some escaped, but not all) #1", + "data": "/~0~", + "valid": false + }, + { + "description": "not a valid JSON-pointer (some escaped, but not all) #2", + "data": "/~0/~", + "valid": false + }, + { + "description": "not a valid JSON-pointer (wrong escape character) #1", + "data": "/~2", + "valid": false + }, + { + "description": "not a valid JSON-pointer (wrong escape character) #2", + "data": "/~-1", + "valid": false + }, + { + "description": "not a valid JSON-pointer (multiple characters not escaped)", + "data": "/~~", + "valid": false + }, + { + "description": "not a valid JSON-pointer (isn't empty nor starts with /) #1", + "data": "a", + "valid": false + }, + { + "description": "not a valid JSON-pointer (isn't empty nor starts with /) #2", + "data": "0", + "valid": false + }, + { + "description": "not a valid JSON-pointer (isn't empty nor starts with /) #3", + "data": "a/a", + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/regex.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/regex.json new file mode 100644 index 000000000..d99d021ec --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/regex.json @@ -0,0 +1,18 @@ +[ + { + "description": "validation of regular expressions", + "schema": {"format": "regex"}, + "tests": [ + { + "description": "a valid regular expression", + "data": "([abc])+\\s+$", + "valid": true + }, + { + "description": "a regular expression with unclosed parens is invalid", + "data": "^(abc]", + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/relative-json-pointer.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/relative-json-pointer.json new file mode 100644 index 000000000..22fb14e07 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/relative-json-pointer.json @@ -0,0 +1,53 @@ +[ + { + "description": "validation of Relative JSON Pointers (RJP)", + "schema": {"format": "relative-json-pointer"}, + "tests": [ + { + "description": "a valid upwards RJP", + "data": "1", + "valid": true + }, + { + "description": "a valid downwards RJP", + "data": "0/foo/bar", + "valid": true + }, + { + "description": "a valid up and then down RJP, with array index", + "data": "2/0/baz/1/zip", + "valid": true + }, + { + "description": "a valid RJP taking the member or index name", + "data": "0#", + "valid": true + }, + { + "description": "an invalid RJP that is a valid JSON Pointer", + "data": "/foo/bar", + "valid": false + }, + { + "description": "negative prefix", + "data": "-1/foo/bar", + "valid": false + }, + { + "description": "## is not a valid json-pointer", + "data": "0##", + "valid": false + }, + { + "description": "zero cannot be followed by other digits, plus json-pointer", + "data": "01/a", + "valid": false + }, + { + "description": "zero cannot be followed by other digits, plus octothorpe", + "data": "01#", + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/time.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/time.json new file mode 100644 index 000000000..0a8da0e9f --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/time.json @@ -0,0 +1,168 @@ +[ + { + "description": "validation of time strings", + "schema": {"format": "time"}, + "tests": [ + { + "description": "a valid time string", + "data": "08:30:06Z", + "valid": true + }, + { + "description": "a valid time string with leap second, Zulu", + "data": "23:59:60Z", + "valid": true + }, + { + "description": "invalid leap second, Zulu (wrong hour)", + "data": "22:59:60Z", + "valid": false + }, + { + "description": "invalid leap second, Zulu (wrong minute)", + "data": "23:58:60Z", + "valid": false + }, + { + "description": "valid leap second, zero time-offset", + "data": "23:59:60+00:00", + "valid": true + }, + { + "description": "invalid leap second, zero time-offset (wrong hour)", + "data": "22:59:60+00:00", + "valid": false + }, + { + "description": "invalid leap second, zero time-offset (wrong minute)", + "data": "23:58:60+00:00", + "valid": false + }, + { + "description": "valid leap second, positive time-offset", + "data": "01:29:60+01:30", + "valid": true + }, + { + "description": "valid leap second, large positive time-offset", + "data": "23:29:60+23:30", + "valid": true + }, + { + "description": "invalid leap second, positive time-offset (wrong hour)", + "data": "23:59:60+01:00", + "valid": false + }, + { + "description": "invalid leap second, positive time-offset (wrong minute)", + "data": "23:59:60+00:30", + "valid": false + }, + { + "description": "valid leap second, negative time-offset", + "data": "15:59:60-08:00", + "valid": true + }, + { + "description": "valid leap second, large negative time-offset", + "data": "00:29:60-23:30", + "valid": true + }, + { + "description": "invalid leap second, negative time-offset (wrong hour)", + "data": "23:59:60-01:00", + "valid": false + }, + { + "description": "invalid leap second, negative time-offset (wrong minute)", + "data": "23:59:60-00:30", + "valid": false + }, + { + "description": "a valid time string with second fraction", + "data": "23:20:50.52Z", + "valid": true + }, + { + "description": "a valid time string with precise second fraction", + "data": "08:30:06.283185Z", + "valid": true + }, + { + "description": "a valid time string with plus offset", + "data": "08:30:06+00:20", + "valid": true + }, + { + "description": "a valid time string with minus offset", + "data": "08:30:06-08:00", + "valid": true + }, + { + "description": "a valid time string with case-insensitive Z", + "data": "08:30:06z", + "valid": true + }, + { + "description": "an invalid time string with invalid hour", + "data": "24:00:00Z", + "valid": false + }, + { + "description": "an invalid time string with invalid minute", + "data": "00:60:00Z", + "valid": false + }, + { + "description": "an invalid time string with invalid second", + "data": "00:00:61Z", + "valid": false + }, + { + "description": "an invalid time string with invalid leap second (wrong hour)", + "data": "22:59:60Z", + "valid": false + }, + { + "description": "an invalid time string with invalid leap second (wrong minute)", + "data": "23:58:60Z", + "valid": false + }, + { + "description": "an invalid time string with invalid time numoffset hour", + "data": "01:02:03+24:00", + "valid": false + }, + { + "description": "an invalid time string with invalid time numoffset minute", + "data": "01:02:03+00:60", + "valid": false + }, + { + "description": "an invalid time string with invalid time with both Z and numoffset", + "data": "01:02:03Z+00:30", + "valid": false + }, + { + "description": "an invalid offset indicator", + "data": "08:30:06 PST", + "valid": false + }, + { + "description": "only RFC3339 not all of ISO 8601 are valid", + "data": "01:01:01,1111", + "valid": false + }, + { + "description": "no time offset", + "data": "12:00:00", + "valid": false + }, + { + "description": "non-ascii digits should be rejected", + "data": "1২:00:00Z", + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/uri-reference.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/uri-reference.json new file mode 100644 index 000000000..e4c9eef63 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/uri-reference.json @@ -0,0 +1,43 @@ +[ + { + "description": "validation of URI References", + "schema": {"format": "uri-reference"}, + "tests": [ + { + "description": "a valid URI", + "data": "http://foo.bar/?baz=qux#quux", + "valid": true + }, + { + "description": "a valid protocol-relative URI Reference", + "data": "//foo.bar/?baz=qux#quux", + "valid": true + }, + { + "description": "a valid relative URI Reference", + "data": "/abc", + "valid": true + }, + { + "description": "an invalid URI Reference", + "data": "\\\\WINDOWS\\fileshare", + "valid": false + }, + { + "description": "a valid URI Reference", + "data": "abc", + "valid": true + }, + { + "description": "a valid URI fragment", + "data": "#fragment", + "valid": true + }, + { + "description": "an invalid URI fragment", + "data": "#frag\\ment", + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/uri-template.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/uri-template.json new file mode 100644 index 000000000..33ab76ee7 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/uri-template.json @@ -0,0 +1,28 @@ +[ + { + "description": "format: uri-template", + "schema": {"format": "uri-template"}, + "tests": [ + { + "description": "a valid uri-template", + "data": "http://example.com/dictionary/{term:1}/{term}", + "valid": true + }, + { + "description": "an invalid uri-template", + "data": "http://example.com/dictionary/{term:1}/{term", + "valid": false + }, + { + "description": "a valid uri-template without variables", + "data": "http://example.com/dictionary", + "valid": true + }, + { + "description": "a valid relative uri-template", + "data": "dictionary/{term:1}/{term}", + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/uri.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/uri.json new file mode 100644 index 000000000..58d308574 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/uri.json @@ -0,0 +1,108 @@ +[ + { + "description": "validation of URIs", + "schema": {"format": "uri"}, + "tests": [ + { + "description": "a valid URL with anchor tag", + "data": "http://foo.bar/?baz=qux#quux", + "valid": true + }, + { + "description": "a valid URL with anchor tag and parentheses", + "data": "http://foo.com/blah_(wikipedia)_blah#cite-1", + "valid": true + }, + { + "description": "a valid URL with URL-encoded stuff", + "data": "http://foo.bar/?q=Test%20URL-encoded%20stuff", + "valid": true + }, + { + "description": "a valid puny-coded URL ", + "data": "http://xn--nw2a.xn--j6w193g/", + "valid": true + }, + { + "description": "a valid URL with many special characters", + "data": "http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com", + "valid": true + }, + { + "description": "a valid URL based on IPv4", + "data": "http://223.255.255.254", + "valid": true + }, + { + "description": "a valid URL with ftp scheme", + "data": "ftp://ftp.is.co.za/rfc/rfc1808.txt", + "valid": true + }, + { + "description": "a valid URL for a simple text file", + "data": "http://www.ietf.org/rfc/rfc2396.txt", + "valid": true + }, + { + "description": "a valid URL ", + "data": "ldap://[2001:db8::7]/c=GB?objectClass?one", + "valid": true + }, + { + "description": "a valid mailto URI", + "data": "mailto:John.Doe@example.com", + "valid": true + }, + { + "description": "a valid newsgroup URI", + "data": "news:comp.infosystems.www.servers.unix", + "valid": true + }, + { + "description": "a valid tel URI", + "data": "tel:+1-816-555-1212", + "valid": true + }, + { + "description": "a valid URN", + "data": "urn:oasis:names:specification:docbook:dtd:xml:4.1.2", + "valid": true + }, + { + "description": "an invalid protocol-relative URI Reference", + "data": "//foo.bar/?baz=qux#quux", + "valid": false + }, + { + "description": "an invalid relative URI Reference", + "data": "/abc", + "valid": false + }, + { + "description": "an invalid URI", + "data": "\\\\WINDOWS\\fileshare", + "valid": false + }, + { + "description": "an invalid URI though valid URI reference", + "data": "abc", + "valid": false + }, + { + "description": "an invalid URI with spaces", + "data": "http:// shouldfail.com", + "valid": false + }, + { + "description": "an invalid URI with spaces and missing scheme", + "data": ":// should fail", + "valid": false + }, + { + "description": "an invalid URI with comma in scheme", + "data": "bar,baz:foo", + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/uuid.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/uuid.json new file mode 100644 index 000000000..e54cbc0f7 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/format/uuid.json @@ -0,0 +1,85 @@ +[ + { + "description": "uuid format", + "schema": { + "format": "uuid" + }, + "tests": [ + { + "description": "all upper-case", + "data": "2EB8AA08-AA98-11EA-B4AA-73B441D16380", + "valid": true + }, + { + "description": "all lower-case", + "data": "2eb8aa08-aa98-11ea-b4aa-73b441d16380", + "valid": true + }, + { + "description": "mixed case", + "data": "2eb8aa08-AA98-11ea-B4Aa-73B441D16380", + "valid": true + }, + { + "description": "all zeroes is valid", + "data": "00000000-0000-0000-0000-000000000000", + "valid": true + }, + { + "description": "wrong length", + "data": "2eb8aa08-aa98-11ea-b4aa-73b441d1638", + "valid": false + }, + { + "description": "missing section", + "data": "2eb8aa08-aa98-11ea-73b441d16380", + "valid": false + }, + { + "description": "bad characters (not hex)", + "data": "2eb8aa08-aa98-11ea-b4ga-73b441d16380", + "valid": false + }, + { + "description": "no dashes", + "data": "2eb8aa08aa9811eab4aa73b441d16380", + "valid": false + }, + { + "description": "too few dashes", + "data": "2eb8aa08aa98-11ea-b4aa73b441d16380", + "valid": false + }, + { + "description": "too many dashes", + "data": "2eb8-aa08-aa98-11ea-b4aa73b44-1d16380", + "valid": false + }, + { + "description": "dashes in the wrong spot", + "data": "2eb8aa08aa9811eab4aa73b441d16380----", + "valid": false + }, + { + "description": "valid version 4", + "data": "98d80576-482e-427f-8434-7f86890ab222", + "valid": true + }, + { + "description": "valid version 5", + "data": "99c17cbb-656f-564a-940f-1a4568f03487", + "valid": true + }, + { + "description": "hypothetical version 6", + "data": "99c17cbb-656f-664a-940f-1a4568f03487", + "valid": true + }, + { + "description": "hypothetical version 15", + "data": "99c17cbb-656f-f64a-940f-1a4568f03487", + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/non-bmp-regex.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/non-bmp-regex.json new file mode 100644 index 000000000..dd67af2b2 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/non-bmp-regex.json @@ -0,0 +1,82 @@ +[ + { + "description": "Proper UTF-16 surrogate pair handling: pattern", + "comment": "Optional because .Net doesn't correctly handle 32-bit Unicode characters", + "schema": { "pattern": "^🐲*$" }, + "tests": [ + { + "description": "matches empty", + "data": "", + "valid": true + }, + { + "description": "matches single", + "data": "🐲", + "valid": true + }, + { + "description": "matches two", + "data": "🐲🐲", + "valid": true + }, + { + "description": "doesn't match one", + "data": "🐉", + "valid": false + }, + { + "description": "doesn't match two", + "data": "🐉🐉", + "valid": false + }, + { + "description": "doesn't match one ASCII", + "data": "D", + "valid": false + }, + { + "description": "doesn't match two ASCII", + "data": "DD", + "valid": false + } + ] + }, + { + "description": "Proper UTF-16 surrogate pair handling: patternProperties", + "comment": "Optional because .Net doesn't correctly handle 32-bit Unicode characters", + "schema": { + "patternProperties": { + "^🐲*$": { + "type": "integer" + } + } + }, + "tests": [ + { + "description": "matches empty", + "data": { "": 1 }, + "valid": true + }, + { + "description": "matches single", + "data": { "🐲": 1 }, + "valid": true + }, + { + "description": "matches two", + "data": { "🐲🐲": 1 }, + "valid": true + }, + { + "description": "doesn't match one", + "data": { "🐲": "hello" }, + "valid": false + }, + { + "description": "doesn't match two", + "data": { "🐲🐲": "hello" }, + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/unicode.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/unicode.json new file mode 100644 index 000000000..1dc5940dc --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/optional/unicode.json @@ -0,0 +1,146 @@ +[ + { + "description": "unicode semantics should be used for all pattern matching", + "schema": { "pattern": "\\wcole" }, + "tests": [ + { + "description": "literal unicode character in json string", + "data": "Les hivers de mon enfance étaient des saisons longues, longues. Nous vivions en trois lieux: l'école, l'église et la patinoire; mais la vraie vie était sur la patinoire.", + "valid": true + }, + { + "description": "unicode character in hex format in string", + "data": "Les hivers de mon enfance étaient des saisons longues, longues. Nous vivions en trois lieux: l'\u00e9cole, l'église et la patinoire; mais la vraie vie était sur la patinoire.", + "valid": true + }, + { + "description": "unicode matching is case-sensitive", + "data": "LES HIVERS DE MON ENFANCE ÉTAIENT DES SAISONS LONGUES, LONGUES. NOUS VIVIONS EN TROIS LIEUX: L'ÉCOLE, L'ÉGLISE ET LA PATINOIRE; MAIS LA VRAIE VIE ÉTAIT SUR LA PATINOIRE.", + "valid": false + } + ] + }, + { + "description": "unicode characters do not match ascii ranges", + "schema": { "pattern": "[a-z]cole" }, + "tests": [ + { + "description": "literal unicode character in json string", + "data": "Les hivers de mon enfance étaient des saisons longues, longues. Nous vivions en trois lieux: l'école, l'église et la patinoire; mais la vraie vie était sur la patinoire.", + "valid": false + }, + { + "description": "unicode character in hex format in string", + "data": "Les hivers de mon enfance étaient des saisons longues, longues. Nous vivions en trois lieux: l'\u00e9cole, l'église et la patinoire; mais la vraie vie était sur la patinoire.", + "valid": false + }, + { + "description": "ascii characters match", + "data": "Les hivers de mon enfance etaient des saisons longues, longues. Nous vivions en trois lieux: l'ecole, l'eglise et la patinoire; mais la vraie vie etait sur la patinoire.", + "valid": true + } + ] + }, + { + "description": "unicode digits are more than 0 through 9", + "schema": { "pattern": "^\\d+$" }, + "tests": [ + { + "description": "ascii digits", + "data": "42", + "valid": true + }, + { + "description": "ascii non-digits", + "data": "-%#", + "valid": false + }, + { + "description": "non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO)", + "data": "৪২", + "valid": true + } + ] + }, + { + "description": "unicode semantics should be used for all patternProperties matching", + "schema": { + "type": "object", + "patternProperties": { + "\\wcole": true + }, + "additionalProperties": false + }, + "tests": [ + { + "description": "literal unicode character in json string", + "data": { "l'école": "pas de vraie vie" }, + "valid": true + }, + { + "description": "unicode character in hex format in string", + "data": { "l'\u00e9cole": "pas de vraie vie" }, + "valid": true + }, + { + "description": "unicode matching is case-sensitive", + "data": { "L'ÉCOLE": "PAS DE VRAIE VIE" }, + "valid": false + } + ] + }, + { + "description": "unicode characters do not match ascii ranges", + "schema": { + "type": "object", + "patternProperties": { + "[a-z]cole": true + }, + "additionalProperties": false + }, + "tests": [ + { + "description": "literal unicode character in json string", + "data": { "l'école": "pas de vraie vie" }, + "valid": false + }, + { + "description": "unicode character in hex format in string", + "data": { "l'\u00e9cole": "pas de vraie vie" }, + "valid": false + }, + { + "description": "ascii characters match", + "data": { "l'ecole": "pas de vraie vie" }, + "valid": true + } + ] + }, + { + "description": "unicode digits are more than 0 through 9", + "schema": { + "type": "object", + "patternProperties": { + "^\\d+$": true + }, + "additionalProperties": false + }, + "tests": [ + { + "description": "ascii digits", + "data": { "42": "life, the universe, and everything" }, + "valid": true + }, + { + "description": "ascii non-digits", + "data": { "-%#": "spending the year dead for tax reasons" }, + "valid": false + }, + { + "description": "non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO)", + "data": { "৪২": "khajit has wares if you have coin" }, + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/pattern.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/pattern.json new file mode 100644 index 000000000..92db0f971 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/pattern.json @@ -0,0 +1,59 @@ +[ + { + "description": "pattern validation", + "schema": {"pattern": "^a*$"}, + "tests": [ + { + "description": "a matching pattern is valid", + "data": "aaa", + "valid": true + }, + { + "description": "a non-matching pattern is invalid", + "data": "abc", + "valid": false + }, + { + "description": "ignores booleans", + "data": true, + "valid": true + }, + { + "description": "ignores integers", + "data": 123, + "valid": true + }, + { + "description": "ignores floats", + "data": 1.0, + "valid": true + }, + { + "description": "ignores objects", + "data": {}, + "valid": true + }, + { + "description": "ignores arrays", + "data": [], + "valid": true + }, + { + "description": "ignores null", + "data": null, + "valid": true + } + ] + }, + { + "description": "pattern is not anchored", + "schema": {"pattern": "a+"}, + "tests": [ + { + "description": "matches a substring", + "data": "xxaayy", + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/patternProperties.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/patternProperties.json new file mode 100644 index 000000000..c10ffcc05 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/patternProperties.json @@ -0,0 +1,156 @@ +[ + { + "description": + "patternProperties validates properties matching a regex", + "schema": { + "patternProperties": { + "f.*o": {"type": "integer"} + } + }, + "tests": [ + { + "description": "a single valid match is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "multiple valid matches is valid", + "data": {"foo": 1, "foooooo" : 2}, + "valid": true + }, + { + "description": "a single invalid match is invalid", + "data": {"foo": "bar", "fooooo": 2}, + "valid": false + }, + { + "description": "multiple invalid matches is invalid", + "data": {"foo": "bar", "foooooo" : "baz"}, + "valid": false + }, + { + "description": "ignores arrays", + "data": ["foo"], + "valid": true + }, + { + "description": "ignores strings", + "data": "foo", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + } + ] + }, + { + "description": "multiple simultaneous patternProperties are validated", + "schema": { + "patternProperties": { + "a*": {"type": "integer"}, + "aaa*": {"maximum": 20} + } + }, + "tests": [ + { + "description": "a single valid match is valid", + "data": {"a": 21}, + "valid": true + }, + { + "description": "a simultaneous match is valid", + "data": {"aaaa": 18}, + "valid": true + }, + { + "description": "multiple matches is valid", + "data": {"a": 21, "aaaa": 18}, + "valid": true + }, + { + "description": "an invalid due to one is invalid", + "data": {"a": "bar"}, + "valid": false + }, + { + "description": "an invalid due to the other is invalid", + "data": {"aaaa": 31}, + "valid": false + }, + { + "description": "an invalid due to both is invalid", + "data": {"aaa": "foo", "aaaa": 31}, + "valid": false + } + ] + }, + { + "description": "regexes are not anchored by default and are case sensitive", + "schema": { + "patternProperties": { + "[0-9]{2,}": { "type": "boolean" }, + "X_": { "type": "string" } + } + }, + "tests": [ + { + "description": "non recognized members are ignored", + "data": { "answer 1": "42" }, + "valid": true + }, + { + "description": "recognized members are accounted for", + "data": { "a31b": null }, + "valid": false + }, + { + "description": "regexes are case sensitive", + "data": { "a_x_3": 3 }, + "valid": true + }, + { + "description": "regexes are case sensitive, 2", + "data": { "a_X_3": 3 }, + "valid": false + } + ] + }, + { + "description": "patternProperties with boolean schemas", + "schema": { + "patternProperties": { + "f.*": true, + "b.*": false + } + }, + "tests": [ + { + "description": "object with property matching schema true is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "object with property matching schema false is invalid", + "data": {"bar": 2}, + "valid": false + }, + { + "description": "object with both properties is invalid", + "data": {"foo": 1, "bar": 2}, + "valid": false + }, + { + "description": "object with a property matching both true and false is invalid", + "data": {"foobar":1}, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/properties.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/properties.json new file mode 100644 index 000000000..b86c18198 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/properties.json @@ -0,0 +1,167 @@ +[ + { + "description": "object properties validation", + "schema": { + "properties": { + "foo": {"type": "integer"}, + "bar": {"type": "string"} + } + }, + "tests": [ + { + "description": "both properties present and valid is valid", + "data": {"foo": 1, "bar": "baz"}, + "valid": true + }, + { + "description": "one property invalid is invalid", + "data": {"foo": 1, "bar": {}}, + "valid": false + }, + { + "description": "both properties invalid is invalid", + "data": {"foo": [], "bar": {}}, + "valid": false + }, + { + "description": "doesn't invalidate other properties", + "data": {"quux": []}, + "valid": true + }, + { + "description": "ignores arrays", + "data": [], + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + } + ] + }, + { + "description": + "properties, patternProperties, additionalProperties interaction", + "schema": { + "properties": { + "foo": {"type": "array", "maxItems": 3}, + "bar": {"type": "array"} + }, + "patternProperties": {"f.o": {"minItems": 2}}, + "additionalProperties": {"type": "integer"} + }, + "tests": [ + { + "description": "property validates property", + "data": {"foo": [1, 2]}, + "valid": true + }, + { + "description": "property invalidates property", + "data": {"foo": [1, 2, 3, 4]}, + "valid": false + }, + { + "description": "patternProperty invalidates property", + "data": {"foo": []}, + "valid": false + }, + { + "description": "patternProperty validates nonproperty", + "data": {"fxo": [1, 2]}, + "valid": true + }, + { + "description": "patternProperty invalidates nonproperty", + "data": {"fxo": []}, + "valid": false + }, + { + "description": "additionalProperty ignores property", + "data": {"bar": []}, + "valid": true + }, + { + "description": "additionalProperty validates others", + "data": {"quux": 3}, + "valid": true + }, + { + "description": "additionalProperty invalidates others", + "data": {"quux": "foo"}, + "valid": false + } + ] + }, + { + "description": "properties with boolean schema", + "schema": { + "properties": { + "foo": true, + "bar": false + } + }, + "tests": [ + { + "description": "no property present is valid", + "data": {}, + "valid": true + }, + { + "description": "only 'true' property present is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "only 'false' property present is invalid", + "data": {"bar": 2}, + "valid": false + }, + { + "description": "both properties present is invalid", + "data": {"foo": 1, "bar": 2}, + "valid": false + } + ] + }, + { + "description": "properties with escaped characters", + "schema": { + "properties": { + "foo\nbar": {"type": "number"}, + "foo\"bar": {"type": "number"}, + "foo\\bar": {"type": "number"}, + "foo\rbar": {"type": "number"}, + "foo\tbar": {"type": "number"}, + "foo\fbar": {"type": "number"} + } + }, + "tests": [ + { + "description": "object with all numbers is valid", + "data": { + "foo\nbar": 1, + "foo\"bar": 1, + "foo\\bar": 1, + "foo\rbar": 1, + "foo\tbar": 1, + "foo\fbar": 1 + }, + "valid": true + }, + { + "description": "object with strings is invalid", + "data": { + "foo\nbar": "1", + "foo\"bar": "1", + "foo\\bar": "1", + "foo\rbar": "1", + "foo\tbar": "1", + "foo\fbar": "1" + }, + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/propertyNames.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/propertyNames.json new file mode 100644 index 000000000..f0788e649 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/propertyNames.json @@ -0,0 +1,107 @@ +[ + { + "description": "propertyNames validation", + "schema": { + "propertyNames": {"maxLength": 3} + }, + "tests": [ + { + "description": "all property names valid", + "data": { + "f": {}, + "foo": {} + }, + "valid": true + }, + { + "description": "some property names invalid", + "data": { + "foo": {}, + "foobar": {} + }, + "valid": false + }, + { + "description": "object without properties is valid", + "data": {}, + "valid": true + }, + { + "description": "ignores arrays", + "data": [1, 2, 3, 4], + "valid": true + }, + { + "description": "ignores strings", + "data": "foobar", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + } + ] + }, + { + "description": "propertyNames validation with pattern", + "schema": { + "propertyNames": { "pattern": "^a+$" } + }, + "tests": [ + { + "description": "matching property names valid", + "data": { + "a": {}, + "aa": {}, + "aaa": {} + }, + "valid": true + }, + { + "description": "non-matching property name is invalid", + "data": { + "aaA": {} + }, + "valid": false + }, + { + "description": "object without properties is valid", + "data": {}, + "valid": true + } + ] + }, + { + "description": "propertyNames with boolean schema true", + "schema": {"propertyNames": true}, + "tests": [ + { + "description": "object with any properties is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] + }, + { + "description": "propertyNames with boolean schema false", + "schema": {"propertyNames": false}, + "tests": [ + { + "description": "object with any properties is invalid", + "data": {"foo": 1}, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/ref.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/ref.json new file mode 100644 index 000000000..900ebb025 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/ref.json @@ -0,0 +1,612 @@ +[ + { + "description": "root pointer ref", + "schema": { + "properties": { + "foo": {"$ref": "#"} + }, + "additionalProperties": false + }, + "tests": [ + { + "description": "match", + "data": {"foo": false}, + "valid": true + }, + { + "description": "recursive match", + "data": {"foo": {"foo": false}}, + "valid": true + }, + { + "description": "mismatch", + "data": {"bar": false}, + "valid": false + }, + { + "description": "recursive mismatch", + "data": {"foo": {"bar": false}}, + "valid": false + } + ] + }, + { + "description": "relative pointer ref to object", + "schema": { + "properties": { + "foo": {"type": "integer"}, + "bar": {"$ref": "#/properties/foo"} + } + }, + "tests": [ + { + "description": "match", + "data": {"bar": 3}, + "valid": true + }, + { + "description": "mismatch", + "data": {"bar": true}, + "valid": false + } + ] + }, + { + "description": "relative pointer ref to array", + "schema": { + "items": [ + {"type": "integer"}, + {"$ref": "#/items/0"} + ] + }, + "tests": [ + { + "description": "match array", + "data": [1, 2], + "valid": true + }, + { + "description": "mismatch array", + "data": [1, "foo"], + "valid": false + } + ] + }, + { + "description": "escaped pointer ref", + "schema": { + "definitions": { + "tilde~field": {"type": "integer"}, + "slash/field": {"type": "integer"}, + "percent%field": {"type": "integer"} + }, + "properties": { + "tilde": {"$ref": "#/definitions/tilde~0field"}, + "slash": {"$ref": "#/definitions/slash~1field"}, + "percent": {"$ref": "#/definitions/percent%25field"} + } + }, + "tests": [ + { + "description": "slash invalid", + "data": {"slash": "aoeu"}, + "valid": false + }, + { + "description": "tilde invalid", + "data": {"tilde": "aoeu"}, + "valid": false + }, + { + "description": "percent invalid", + "data": {"percent": "aoeu"}, + "valid": false + }, + { + "description": "slash valid", + "data": {"slash": 123}, + "valid": true + }, + { + "description": "tilde valid", + "data": {"tilde": 123}, + "valid": true + }, + { + "description": "percent valid", + "data": {"percent": 123}, + "valid": true + } + ] + }, + { + "description": "nested refs", + "schema": { + "definitions": { + "a": {"type": "integer"}, + "b": {"$ref": "#/definitions/a"}, + "c": {"$ref": "#/definitions/b"} + }, + "allOf": [{ "$ref": "#/definitions/c" }] + }, + "tests": [ + { + "description": "nested ref valid", + "data": 5, + "valid": true + }, + { + "description": "nested ref invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "ref overrides any sibling keywords", + "schema": { + "definitions": { + "reffed": { + "type": "array" + } + }, + "properties": { + "foo": { + "$ref": "#/definitions/reffed", + "maxItems": 2 + } + } + }, + "tests": [ + { + "description": "ref valid", + "data": { "foo": [] }, + "valid": true + }, + { + "description": "ref valid, maxItems ignored", + "data": { "foo": [ 1, 2, 3] }, + "valid": true + }, + { + "description": "ref invalid", + "data": { "foo": "string" }, + "valid": false + } + ] + }, + { + "description": "$ref prevents a sibling $id from changing the base uri", + "schema": { + "$id": "http://localhost:1234/sibling_id/base/", + "definitions": { + "foo": { + "$id": "http://localhost:1234/sibling_id/foo.json", + "minimum": 2 + }, + "base_foo": { + "$comment": "this canonical uri is http://localhost:1234/sibling_id/base/foo.json", + "$id": "foo.json", + "minimum": 5 + } + }, + "allOf": [ + { + "$comment": "$ref resolves to http://localhost:1234/sibling_id/base/foo.json, not ttp://localhost:1234/sibling_id/foo.json", + "$id": "http://localhost:1234/sibling_id/", + "$ref": "foo.json" + } + ] + }, + "tests": [ + { + "description": "$ref resolves to /definitions/foo, data validates", + "data": 10, + "valid": true + }, + { + "description": "$ref resolves to /definitions/foo, data does not validate", + "data": 1, + "valid": false + } + ] + }, + { + "description": "remote ref, containing refs itself", + "schema": {"$ref": "http://json-schema.org/draft-07/schema#"}, + "tests": [ + { + "description": "remote ref valid", + "data": {"minLength": 1}, + "valid": true + }, + { + "description": "remote ref invalid", + "data": {"minLength": -1}, + "valid": false + } + ] + }, + { + "description": "property named $ref that is not a reference", + "schema": { + "properties": { + "$ref": {"type": "string"} + } + }, + "tests": [ + { + "description": "property named $ref valid", + "data": {"$ref": "a"}, + "valid": true + }, + { + "description": "property named $ref invalid", + "data": {"$ref": 2}, + "valid": false + } + ] + }, + { + "description": "property named $ref, containing an actual $ref", + "schema": { + "properties": { + "$ref": {"$ref": "#/definitions/is-string"} + }, + "definitions": { + "is-string": { + "type": "string" + } + } + }, + "tests": [ + { + "description": "property named $ref valid", + "data": {"$ref": "a"}, + "valid": true + }, + { + "description": "property named $ref invalid", + "data": {"$ref": 2}, + "valid": false + } + ] + }, + { + "description": "$ref to boolean schema true", + "schema": { + "allOf": [{ "$ref": "#/definitions/bool" }], + "definitions": { + "bool": true + } + }, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "$ref to boolean schema false", + "schema": { + "allOf": [{ "$ref": "#/definitions/bool" }], + "definitions": { + "bool": false + } + }, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "Recursive references between schemas", + "schema": { + "$id": "http://localhost:1234/tree", + "description": "tree of nodes", + "type": "object", + "properties": { + "meta": {"type": "string"}, + "nodes": { + "type": "array", + "items": {"$ref": "node"} + } + }, + "required": ["meta", "nodes"], + "definitions": { + "node": { + "$id": "http://localhost:1234/node", + "description": "node", + "type": "object", + "properties": { + "value": {"type": "number"}, + "subtree": {"$ref": "tree"} + }, + "required": ["value"] + } + } + }, + "tests": [ + { + "description": "valid tree", + "data": { + "meta": "root", + "nodes": [ + { + "value": 1, + "subtree": { + "meta": "child", + "nodes": [ + {"value": 1.1}, + {"value": 1.2} + ] + } + }, + { + "value": 2, + "subtree": { + "meta": "child", + "nodes": [ + {"value": 2.1}, + {"value": 2.2} + ] + } + } + ] + }, + "valid": true + }, + { + "description": "invalid tree", + "data": { + "meta": "root", + "nodes": [ + { + "value": 1, + "subtree": { + "meta": "child", + "nodes": [ + {"value": "string is invalid"}, + {"value": 1.2} + ] + } + }, + { + "value": 2, + "subtree": { + "meta": "child", + "nodes": [ + {"value": 2.1}, + {"value": 2.2} + ] + } + } + ] + }, + "valid": false + } + ] + }, + { + "description": "refs with quote", + "schema": { + "properties": { + "foo\"bar": {"$ref": "#/definitions/foo%22bar"} + }, + "definitions": { + "foo\"bar": {"type": "number"} + } + }, + "tests": [ + { + "description": "object with numbers is valid", + "data": { + "foo\"bar": 1 + }, + "valid": true + }, + { + "description": "object with strings is invalid", + "data": { + "foo\"bar": "1" + }, + "valid": false + } + ] + }, + { + "description": "Location-independent identifier", + "schema": { + "allOf": [{ + "$ref": "#foo" + }], + "definitions": { + "A": { + "$id": "#foo", + "type": "integer" + } + } + }, + "tests": [ + { + "data": 1, + "description": "match", + "valid": true + }, + { + "data": "a", + "description": "mismatch", + "valid": false + } + ] + }, + { + "description": "Location-independent identifier with base URI change in subschema", + "schema": { + "$id": "http://localhost:1234/root", + "allOf": [{ + "$ref": "http://localhost:1234/nested.json#foo" + }], + "definitions": { + "A": { + "$id": "nested.json", + "definitions": { + "B": { + "$id": "#foo", + "type": "integer" + } + } + } + } + }, + "tests": [ + { + "data": 1, + "description": "match", + "valid": true + }, + { + "data": "a", + "description": "mismatch", + "valid": false + } + ] + }, + { + "description": "naive replacement of $ref with its destination is not correct", + "schema": { + "definitions": { + "a_string": { "type": "string" } + }, + "enum": [ + { "$ref": "#/definitions/a_string" } + ] + }, + "tests": [ + { + "description": "do not evaluate the $ref inside the enum, matching any string", + "data": "this is a string", + "valid": false + }, + { + "description": "do not evaluate the $ref inside the enum, definition exact match", + "data": { "type": "string" }, + "valid": false + }, + { + "description": "match the enum exactly", + "data": { "$ref": "#/definitions/a_string" }, + "valid": true + } + ] + }, + { + "description": "refs with relative uris and defs", + "schema": { + "$id": "http://example.com/schema-relative-uri-defs1.json", + "properties": { + "foo": { + "$id": "schema-relative-uri-defs2.json", + "definitions": { + "inner": { + "properties": { + "bar": { "type": "string" } + } + } + }, + "allOf": [ { "$ref": "#/definitions/inner" } ] + } + }, + "allOf": [ { "$ref": "schema-relative-uri-defs2.json" } ] + }, + "tests": [ + { + "description": "invalid on inner field", + "data": { + "foo": { + "bar": 1 + }, + "bar": "a" + }, + "valid": false + }, + { + "description": "invalid on outer field", + "data": { + "foo": { + "bar": "a" + }, + "bar": 1 + }, + "valid": false + }, + { + "description": "valid on both fields", + "data": { + "foo": { + "bar": "a" + }, + "bar": "a" + }, + "valid": true + } + ] + }, + { + "description": "relative refs with absolute uris and defs", + "schema": { + "$id": "http://example.com/schema-refs-absolute-uris-defs1.json", + "properties": { + "foo": { + "$id": "http://example.com/schema-refs-absolute-uris-defs2.json", + "definitions": { + "inner": { + "properties": { + "bar": { "type": "string" } + } + } + }, + "allOf": [ { "$ref": "#/definitions/inner" } ] + } + }, + "allOf": [ { "$ref": "schema-refs-absolute-uris-defs2.json" } ] + }, + "tests": [ + { + "description": "invalid on inner field", + "data": { + "foo": { + "bar": 1 + }, + "bar": "a" + }, + "valid": false + }, + { + "description": "invalid on outer field", + "data": { + "foo": { + "bar": "a" + }, + "bar": 1 + }, + "valid": false + }, + { + "description": "valid on both fields", + "data": { + "foo": { + "bar": "a" + }, + "bar": "a" + }, + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/refRemote.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/refRemote.json new file mode 100644 index 000000000..a2221b213 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/refRemote.json @@ -0,0 +1,196 @@ +[ + { + "description": "remote ref", + "schema": {"$ref": "http://localhost:1234/integer.json"}, + "tests": [ + { + "description": "remote ref valid", + "data": 1, + "valid": true + }, + { + "description": "remote ref invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "fragment within remote ref", + "schema": {"$ref": "http://localhost:1234/subSchemas.json#/integer"}, + "tests": [ + { + "description": "remote fragment valid", + "data": 1, + "valid": true + }, + { + "description": "remote fragment invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "ref within remote ref", + "schema": { + "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + }, + "tests": [ + { + "description": "ref within ref valid", + "data": 1, + "valid": true + }, + { + "description": "ref within ref invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "base URI change", + "schema": { + "$id": "http://localhost:1234/", + "items": { + "$id": "baseUriChange/", + "items": {"$ref": "folderInteger.json"} + } + }, + "tests": [ + { + "description": "base URI change ref valid", + "data": [[1]], + "valid": true + }, + { + "description": "base URI change ref invalid", + "data": [["a"]], + "valid": false + } + ] + }, + { + "description": "base URI change - change folder", + "schema": { + "$id": "http://localhost:1234/scope_change_defs1.json", + "type" : "object", + "properties": { + "list": {"$ref": "#/definitions/baz"} + }, + "definitions": { + "baz": { + "$id": "baseUriChangeFolder/", + "type": "array", + "items": {"$ref": "folderInteger.json"} + } + } + }, + "tests": [ + { + "description": "number is valid", + "data": {"list": [1]}, + "valid": true + }, + { + "description": "string is invalid", + "data": {"list": ["a"]}, + "valid": false + } + ] + }, + { + "description": "base URI change - change folder in subschema", + "schema": { + "$id": "http://localhost:1234/scope_change_defs2.json", + "type" : "object", + "properties": { + "list": {"$ref": "#/definitions/baz/definitions/bar"} + }, + "definitions": { + "baz": { + "$id": "baseUriChangeFolderInSubschema/", + "definitions": { + "bar": { + "type": "array", + "items": {"$ref": "folderInteger.json"} + } + } + } + } + }, + "tests": [ + { + "description": "number is valid", + "data": {"list": [1]}, + "valid": true + }, + { + "description": "string is invalid", + "data": {"list": ["a"]}, + "valid": false + } + ] + }, + { + "description": "root ref in remote ref", + "schema": { + "$id": "http://localhost:1234/object", + "type": "object", + "properties": { + "name": {"$ref": "name.json#/definitions/orNull"} + } + }, + "tests": [ + { + "description": "string is valid", + "data": { + "name": "foo" + }, + "valid": true + }, + { + "description": "null is valid", + "data": { + "name": null + }, + "valid": true + }, + { + "description": "object is invalid", + "data": { + "name": { + "name": null + } + }, + "valid": false + } + ] + }, + { + "description": "remote ref with ref to definitions", + "schema": { + "$id": "http://localhost:1234/schema-remote-ref-ref-defs1.json", + "allOf": [ + { "$ref": "ref-and-definitions.json" } + ] + }, + "tests": [ + { + "description": "invalid", + "data": { + "bar": 1 + }, + "valid": false + }, + { + "description": "valid", + "data": { + "bar": "a" + }, + "valid": true + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/required.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/required.json new file mode 100644 index 000000000..abf18f345 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/required.json @@ -0,0 +1,105 @@ +[ + { + "description": "required validation", + "schema": { + "properties": { + "foo": {}, + "bar": {} + }, + "required": ["foo"] + }, + "tests": [ + { + "description": "present required property is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "non-present required property is invalid", + "data": {"bar": 1}, + "valid": false + }, + { + "description": "ignores arrays", + "data": [], + "valid": true + }, + { + "description": "ignores strings", + "data": "", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + } + ] + }, + { + "description": "required default validation", + "schema": { + "properties": { + "foo": {} + } + }, + "tests": [ + { + "description": "not required by default", + "data": {}, + "valid": true + } + ] + }, + { + "description": "required with empty array", + "schema": { + "properties": { + "foo": {} + }, + "required": [] + }, + "tests": [ + { + "description": "property not required", + "data": {}, + "valid": true + } + ] + }, + { + "description": "required with escaped characters", + "schema": { + "required": [ + "foo\nbar", + "foo\"bar", + "foo\\bar", + "foo\rbar", + "foo\tbar", + "foo\fbar" + ] + }, + "tests": [ + { + "description": "object with all properties present is valid", + "data": { + "foo\nbar": 1, + "foo\"bar": 1, + "foo\\bar": 1, + "foo\rbar": 1, + "foo\tbar": 1, + "foo\fbar": 1 + }, + "valid": true + }, + { + "description": "object with some properties missing is invalid", + "data": { + "foo\nbar": "1", + "foo\"bar": "1" + }, + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/type.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/type.json new file mode 100644 index 000000000..efb4f57bc --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/type.json @@ -0,0 +1,469 @@ +[ + { + "description": "integer type matches integers", + "schema": {"type": "integer"}, + "tests": [ + { + "description": "an integer is an integer", + "data": 1, + "valid": true + }, + { + "description": "a float is not an integer", + "data": 1.1, + "valid": false + }, + { + "description": "a string is not an integer", + "data": "foo", + "valid": false + }, + { + "description": "a string is still not an integer, even if it looks like one", + "data": "1", + "valid": false + }, + { + "description": "an object is not an integer", + "data": {}, + "valid": false + }, + { + "description": "an array is not an integer", + "data": [], + "valid": false + }, + { + "description": "a boolean is not an integer", + "data": true, + "valid": false + }, + { + "description": "null is not an integer", + "data": null, + "valid": false + } + ] + }, + { + "description": "number type matches numbers", + "schema": {"type": "number"}, + "tests": [ + { + "description": "an integer is a number", + "data": 1, + "valid": true + }, + { + "description": "a float with zero fractional part is a number (and an integer)", + "data": 1.0, + "valid": true + }, + { + "description": "a float is a number", + "data": 1.1, + "valid": true + }, + { + "description": "a string is not a number", + "data": "foo", + "valid": false + }, + { + "description": "a string is still not a number, even if it looks like one", + "data": "1", + "valid": false + }, + { + "description": "an object is not a number", + "data": {}, + "valid": false + }, + { + "description": "an array is not a number", + "data": [], + "valid": false + }, + { + "description": "a boolean is not a number", + "data": true, + "valid": false + }, + { + "description": "null is not a number", + "data": null, + "valid": false + } + ] + }, + { + "description": "string type matches strings", + "schema": {"type": "string"}, + "tests": [ + { + "description": "1 is not a string", + "data": 1, + "valid": false + }, + { + "description": "a float is not a string", + "data": 1.1, + "valid": false + }, + { + "description": "a string is a string", + "data": "foo", + "valid": true + }, + { + "description": "a string is still a string, even if it looks like a number", + "data": "1", + "valid": true + }, + { + "description": "an empty string is still a string", + "data": "", + "valid": true + }, + { + "description": "an object is not a string", + "data": {}, + "valid": false + }, + { + "description": "an array is not a string", + "data": [], + "valid": false + }, + { + "description": "a boolean is not a string", + "data": true, + "valid": false + }, + { + "description": "null is not a string", + "data": null, + "valid": false + } + ] + }, + { + "description": "object type matches objects", + "schema": {"type": "object"}, + "tests": [ + { + "description": "an integer is not an object", + "data": 1, + "valid": false + }, + { + "description": "a float is not an object", + "data": 1.1, + "valid": false + }, + { + "description": "a string is not an object", + "data": "foo", + "valid": false + }, + { + "description": "an object is an object", + "data": {}, + "valid": true + }, + { + "description": "an array is not an object", + "data": [], + "valid": false + }, + { + "description": "a boolean is not an object", + "data": true, + "valid": false + }, + { + "description": "null is not an object", + "data": null, + "valid": false + } + ] + }, + { + "description": "array type matches arrays", + "schema": {"type": "array"}, + "tests": [ + { + "description": "an integer is not an array", + "data": 1, + "valid": false + }, + { + "description": "a float is not an array", + "data": 1.1, + "valid": false + }, + { + "description": "a string is not an array", + "data": "foo", + "valid": false + }, + { + "description": "an object is not an array", + "data": {}, + "valid": false + }, + { + "description": "an array is an array", + "data": [], + "valid": true + }, + { + "description": "a boolean is not an array", + "data": true, + "valid": false + }, + { + "description": "null is not an array", + "data": null, + "valid": false + } + ] + }, + { + "description": "boolean type matches booleans", + "schema": {"type": "boolean"}, + "tests": [ + { + "description": "an integer is not a boolean", + "data": 1, + "valid": false + }, + { + "description": "zero is not a boolean", + "data": 0, + "valid": false + }, + { + "description": "a float is not a boolean", + "data": 1.1, + "valid": false + }, + { + "description": "a string is not a boolean", + "data": "foo", + "valid": false + }, + { + "description": "an empty string is not a boolean", + "data": "", + "valid": false + }, + { + "description": "an object is not a boolean", + "data": {}, + "valid": false + }, + { + "description": "an array is not a boolean", + "data": [], + "valid": false + }, + { + "description": "true is a boolean", + "data": true, + "valid": true + }, + { + "description": "false is a boolean", + "data": false, + "valid": true + }, + { + "description": "null is not a boolean", + "data": null, + "valid": false + } + ] + }, + { + "description": "null type matches only the null object", + "schema": {"type": "null"}, + "tests": [ + { + "description": "an integer is not null", + "data": 1, + "valid": false + }, + { + "description": "a float is not null", + "data": 1.1, + "valid": false + }, + { + "description": "zero is not null", + "data": 0, + "valid": false + }, + { + "description": "a string is not null", + "data": "foo", + "valid": false + }, + { + "description": "an empty string is not null", + "data": "", + "valid": false + }, + { + "description": "an object is not null", + "data": {}, + "valid": false + }, + { + "description": "an array is not null", + "data": [], + "valid": false + }, + { + "description": "true is not null", + "data": true, + "valid": false + }, + { + "description": "false is not null", + "data": false, + "valid": false + }, + { + "description": "null is null", + "data": null, + "valid": true + } + ] + }, + { + "description": "multiple types can be specified in an array", + "schema": {"type": ["integer", "string"]}, + "tests": [ + { + "description": "an integer is valid", + "data": 1, + "valid": true + }, + { + "description": "a string is valid", + "data": "foo", + "valid": true + }, + { + "description": "a float is invalid", + "data": 1.1, + "valid": false + }, + { + "description": "an object is invalid", + "data": {}, + "valid": false + }, + { + "description": "an array is invalid", + "data": [], + "valid": false + }, + { + "description": "a boolean is invalid", + "data": true, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + } + ] + }, + { + "description": "type as array with one item", + "schema": { + "type": ["string"] + }, + "tests": [ + { + "description": "string is valid", + "data": "foo", + "valid": true + }, + { + "description": "number is invalid", + "data": 123, + "valid": false + } + ] + }, + { + "description": "type: array or object", + "schema": { + "type": ["array", "object"] + }, + "tests": [ + { + "description": "array is valid", + "data": [1,2,3], + "valid": true + }, + { + "description": "object is valid", + "data": {"foo": 123}, + "valid": true + }, + { + "description": "number is invalid", + "data": 123, + "valid": false + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + } + ] + }, + { + "description": "type: array, object or null", + "schema": { + "type": ["array", "object", "null"] + }, + "tests": [ + { + "description": "array is valid", + "data": [1,2,3], + "valid": true + }, + { + "description": "object is valid", + "data": {"foo": 123}, + "valid": true + }, + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "number is invalid", + "data": 123, + "valid": false + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/uniqueItems.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/uniqueItems.json new file mode 100644 index 000000000..4846c7735 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/uniqueItems.json @@ -0,0 +1,384 @@ +[ + { + "description": "uniqueItems validation", + "schema": {"uniqueItems": true}, + "tests": [ + { + "description": "unique array of integers is valid", + "data": [1, 2], + "valid": true + }, + { + "description": "non-unique array of integers is invalid", + "data": [1, 1], + "valid": false + }, + { + "description": "numbers are unique if mathematically unequal", + "data": [1.0, 1.00, 1], + "valid": false + }, + { + "description": "false is not equal to zero", + "data": [0, false], + "valid": true + }, + { + "description": "true is not equal to one", + "data": [1, true], + "valid": true + }, + { + "description": "unique array of objects is valid", + "data": [{"foo": "bar"}, {"foo": "baz"}], + "valid": true + }, + { + "description": "non-unique array of objects is invalid", + "data": [{"foo": "bar"}, {"foo": "bar"}], + "valid": false + }, + { + "description": "unique array of nested objects is valid", + "data": [ + {"foo": {"bar" : {"baz" : true}}}, + {"foo": {"bar" : {"baz" : false}}} + ], + "valid": true + }, + { + "description": "non-unique array of nested objects is invalid", + "data": [ + {"foo": {"bar" : {"baz" : true}}}, + {"foo": {"bar" : {"baz" : true}}} + ], + "valid": false + }, + { + "description": "unique array of arrays is valid", + "data": [["foo"], ["bar"]], + "valid": true + }, + { + "description": "non-unique array of arrays is invalid", + "data": [["foo"], ["foo"]], + "valid": false + }, + { + "description": "1 and true are unique", + "data": [1, true], + "valid": true + }, + { + "description": "0 and false are unique", + "data": [0, false], + "valid": true + }, + { + "description": "[1] and [true] are unique", + "data": [[1], [true]], + "valid": true + }, + { + "description": "[0] and [false] are unique", + "data": [[0], [false]], + "valid": true + }, + { + "description": "nested [1] and [true] are unique", + "data": [[[1], "foo"], [[true], "foo"]], + "valid": true + }, + { + "description": "nested [0] and [false] are unique", + "data": [[[0], "foo"], [[false], "foo"]], + "valid": true + }, + { + "description": "unique heterogeneous types are valid", + "data": [{}, [1], true, null, 1, "{}"], + "valid": true + }, + { + "description": "non-unique heterogeneous types are invalid", + "data": [{}, [1], true, null, {}, 1], + "valid": false + }, + { + "description": "different objects are unique", + "data": [{"a": 1, "b": 2}, {"a": 2, "b": 1}], + "valid": true + }, + { + "description": "objects are non-unique despite key order", + "data": [{"a": 1, "b": 2}, {"b": 2, "a": 1}], + "valid": false + }, + { + "description": "{\"a\": false} and {\"a\": 0} are unique", + "data": [{"a": false}, {"a": 0}], + "valid": true + }, + { + "description": "{\"a\": true} and {\"a\": 1} are unique", + "data": [{"a": true}, {"a": 1}], + "valid": true + } + ] + }, + { + "description": "uniqueItems with an array of items", + "schema": { + "items": [{"type": "boolean"}, {"type": "boolean"}], + "uniqueItems": true + }, + "tests": [ + { + "description": "[false, true] from items array is valid", + "data": [false, true], + "valid": true + }, + { + "description": "[true, false] from items array is valid", + "data": [true, false], + "valid": true + }, + { + "description": "[false, false] from items array is not valid", + "data": [false, false], + "valid": false + }, + { + "description": "[true, true] from items array is not valid", + "data": [true, true], + "valid": false + }, + { + "description": "unique array extended from [false, true] is valid", + "data": [false, true, "foo", "bar"], + "valid": true + }, + { + "description": "unique array extended from [true, false] is valid", + "data": [true, false, "foo", "bar"], + "valid": true + }, + { + "description": "non-unique array extended from [false, true] is not valid", + "data": [false, true, "foo", "foo"], + "valid": false + }, + { + "description": "non-unique array extended from [true, false] is not valid", + "data": [true, false, "foo", "foo"], + "valid": false + } + ] + }, + { + "description": "uniqueItems with an array of items and additionalItems=false", + "schema": { + "items": [{"type": "boolean"}, {"type": "boolean"}], + "uniqueItems": true, + "additionalItems": false + }, + "tests": [ + { + "description": "[false, true] from items array is valid", + "data": [false, true], + "valid": true + }, + { + "description": "[true, false] from items array is valid", + "data": [true, false], + "valid": true + }, + { + "description": "[false, false] from items array is not valid", + "data": [false, false], + "valid": false + }, + { + "description": "[true, true] from items array is not valid", + "data": [true, true], + "valid": false + }, + { + "description": "extra items are invalid even if unique", + "data": [false, true, null], + "valid": false + } + ] + }, + { + "description": "uniqueItems=false validation", + "schema": { "uniqueItems": false }, + "tests": [ + { + "description": "unique array of integers is valid", + "data": [1, 2], + "valid": true + }, + { + "description": "non-unique array of integers is valid", + "data": [1, 1], + "valid": true + }, + { + "description": "numbers are unique if mathematically unequal", + "data": [1.0, 1.00, 1], + "valid": true + }, + { + "description": "false is not equal to zero", + "data": [0, false], + "valid": true + }, + { + "description": "true is not equal to one", + "data": [1, true], + "valid": true + }, + { + "description": "unique array of objects is valid", + "data": [{"foo": "bar"}, {"foo": "baz"}], + "valid": true + }, + { + "description": "non-unique array of objects is valid", + "data": [{"foo": "bar"}, {"foo": "bar"}], + "valid": true + }, + { + "description": "unique array of nested objects is valid", + "data": [ + {"foo": {"bar" : {"baz" : true}}}, + {"foo": {"bar" : {"baz" : false}}} + ], + "valid": true + }, + { + "description": "non-unique array of nested objects is valid", + "data": [ + {"foo": {"bar" : {"baz" : true}}}, + {"foo": {"bar" : {"baz" : true}}} + ], + "valid": true + }, + { + "description": "unique array of arrays is valid", + "data": [["foo"], ["bar"]], + "valid": true + }, + { + "description": "non-unique array of arrays is valid", + "data": [["foo"], ["foo"]], + "valid": true + }, + { + "description": "1 and true are unique", + "data": [1, true], + "valid": true + }, + { + "description": "0 and false are unique", + "data": [0, false], + "valid": true + }, + { + "description": "unique heterogeneous types are valid", + "data": [{}, [1], true, null, 1], + "valid": true + }, + { + "description": "non-unique heterogeneous types are valid", + "data": [{}, [1], true, null, {}, 1], + "valid": true + } + ] + }, + { + "description": "uniqueItems=false with an array of items", + "schema": { + "items": [{"type": "boolean"}, {"type": "boolean"}], + "uniqueItems": false + }, + "tests": [ + { + "description": "[false, true] from items array is valid", + "data": [false, true], + "valid": true + }, + { + "description": "[true, false] from items array is valid", + "data": [true, false], + "valid": true + }, + { + "description": "[false, false] from items array is valid", + "data": [false, false], + "valid": true + }, + { + "description": "[true, true] from items array is valid", + "data": [true, true], + "valid": true + }, + { + "description": "unique array extended from [false, true] is valid", + "data": [false, true, "foo", "bar"], + "valid": true + }, + { + "description": "unique array extended from [true, false] is valid", + "data": [true, false, "foo", "bar"], + "valid": true + }, + { + "description": "non-unique array extended from [false, true] is valid", + "data": [false, true, "foo", "foo"], + "valid": true + }, + { + "description": "non-unique array extended from [true, false] is valid", + "data": [true, false, "foo", "foo"], + "valid": true + } + ] + }, + { + "description": "uniqueItems=false with an array of items and additionalItems=false", + "schema": { + "items": [{"type": "boolean"}, {"type": "boolean"}], + "uniqueItems": false, + "additionalItems": false + }, + "tests": [ + { + "description": "[false, true] from items array is valid", + "data": [false, true], + "valid": true + }, + { + "description": "[true, false] from items array is valid", + "data": [true, false], + "valid": true + }, + { + "description": "[false, false] from items array is valid", + "data": [false, false], + "valid": true + }, + { + "description": "[true, true] from items array is valid", + "data": [true, true], + "valid": true + }, + { + "description": "extra items are invalid even if unique", + "data": [false, true, null], + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/unknownKeyword.json b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/unknownKeyword.json new file mode 100644 index 000000000..1f58d97e3 --- /dev/null +++ b/libs/jansson_ext/gtest/src/JSON-Schema-Test-Suite/tests/draft7/unknownKeyword.json @@ -0,0 +1,56 @@ +[ + { + "description": "$id inside an unknown keyword is not a real identifier", + "comment": "the implementation must not be confused by an $id in locations we do not know how to parse", + "schema": { + "definitions": { + "id_in_unknown0": { + "not": { + "array_of_schemas": [ + { + "$id": "https://localhost:1234/unknownKeyword/my_identifier.json", + "type": "null" + } + ] + } + }, + "real_id_in_schema": { + "$id": "https://localhost:1234/unknownKeyword/my_identifier.json", + "type": "string" + }, + "id_in_unknown1": { + "not": { + "object_of_schemas": { + "foo": { + "$id": "https://localhost:1234/unknownKeyword/my_identifier.json", + "type": "integer" + } + } + } + } + }, + "anyOf": [ + { "$ref": "#/definitions/id_in_unknown0" }, + { "$ref": "#/definitions/id_in_unknown1" }, + { "$ref": "https://localhost:1234/unknownKeyword/my_identifier.json" } + ] + }, + "tests": [ + { + "description": "type matches second anyOf, which has a real schema in it", + "data": "a string", + "valid": true + }, + { + "description": "type matches non-schema in first anyOf", + "data": null, + "valid": false + }, + { + "description": "type matches non-schema in third anyOf", + "data": 1, + "valid": false + } + ] + } +] diff --git a/libs/jansson_ext/gtest/src/MergePatchErrorInjectionTestSuite.cc b/libs/jansson_ext/gtest/src/MergePatchErrorInjectionTestSuite.cc new file mode 100644 index 000000000..28e214904 --- /dev/null +++ b/libs/jansson_ext/gtest/src/MergePatchErrorInjectionTestSuite.cc @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include + +#include "celix_json_merge_patch.h" +#include "jansson_ei.h" + +/** + * Error-injection tests for the OOM (out-of-memory) handling paths of + * celix_json_merge_patch. + * + * The merge uses one static helper (merge_patch_recursive), so the injected + * caller is matched with `level` = the number of frames between the wrapped + * allocator and the exported celix_json_merge_patch that starts the pipeline: + * - json_deep_copy of target/patch directly from celix_json_merge_patch: + * level 0 + * - json_object/json_null/json_object_set_new from the first + * merge_patch_recursive frame: level 1 + * - json_deep_copy/json_object from a nested merge_patch_recursive frame: + * level 2 + * Every injection targets exactly one call, so the ordinal stays at its + * default of 1. + */ +class JanssonExtMergePatchErrorInjectionTestSuite : public ::testing::Test { +public: + ~JanssonExtMergePatchErrorInjectionTestSuite() noexcept override { + celix_ei_expect_json_deep_copy(nullptr, 0, nullptr); + celix_ei_expect_json_object(nullptr, 0, nullptr); + celix_ei_expect_json_null(nullptr, 0, nullptr); + celix_ei_expect_json_object_set_new(nullptr, 0, 0); + } + +protected: + static json_t* loadJson(const char* text) { + return json_loads(text, JSON_DECODE_ANY, nullptr); + } +}; + +TEST_F(JanssonExtMergePatchErrorInjectionTestSuite, MergePatchTargetCopyDeepCopyFail) { + //Given json_deep_copy is injected to fail while copying the target + json_auto_t* target = loadJson(R"({"a":1})"); + json_auto_t* patch = loadJson(R"({"a":2})"); + ASSERT_NE(nullptr, target); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_deep_copy((void*)celix_json_merge_patch, 0, nullptr); + //Then merging should fail + EXPECT_EQ(nullptr, celix_json_merge_patch(target, patch)); +} + +TEST_F(JanssonExtMergePatchErrorInjectionTestSuite, MergePatchFastPathDeepCopyFail) { + //Given json_deep_copy is injected to fail in the whole-document + //replacement fast path (a non-object patch is deep-copied directly) + json_auto_t* target = loadJson(R"({"a":"b"})"); + json_auto_t* patch = loadJson("[1,2]"); + ASSERT_NE(nullptr, target); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_deep_copy((void*)celix_json_merge_patch, 0, nullptr); + //Then merging should fail + EXPECT_EQ(nullptr, celix_json_merge_patch(target, patch)); +} + +TEST_F(JanssonExtMergePatchErrorInjectionTestSuite, MergePatchValueDeepCopyFail) { + //Given json_deep_copy is injected to fail for a member value being + //replaced (deep copy from the 2nd recursive frame -> level 2; the level-0 + //target copy is not matched and passes) + json_auto_t* target = loadJson(R"({"a":"x"})"); + json_auto_t* patch = loadJson(R"({"a":[1,2]})"); + ASSERT_NE(nullptr, target); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_deep_copy((void*)celix_json_merge_patch, 2, nullptr); + //Then merging should fail + EXPECT_EQ(nullptr, celix_json_merge_patch(target, patch)); +} + +TEST_F(JanssonExtMergePatchErrorInjectionTestSuite, MergePatchTargetNotObjectJsonObjectFail) { + //Given json_object is injected to fail while an object patch converts a + //non-object target to a fresh object (1st recursive frame -> level 1) + json_auto_t* target = loadJson("[1,2]"); + json_auto_t* patch = loadJson(R"({"a":"b"})"); + ASSERT_NE(nullptr, target); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_object((void*)celix_json_merge_patch, 1, nullptr); + //Then merging should fail + EXPECT_EQ(nullptr, celix_json_merge_patch(target, patch)); +} + +TEST_F(JanssonExtMergePatchErrorInjectionTestSuite, MergePatchMemberNotObjectJsonObjectFail) { + //Given json_object is injected to fail while an object member patch + //converts a non-object member to a fresh object (2nd recursive frame -> + //level 2) + json_auto_t* target = loadJson(R"({"a":1})"); + json_auto_t* patch = loadJson(R"({"a":{"b":2}})"); + ASSERT_NE(nullptr, target); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_object((void*)celix_json_merge_patch, 2, nullptr); + //Then merging should fail + EXPECT_EQ(nullptr, celix_json_merge_patch(target, patch)); +} + +TEST_F(JanssonExtMergePatchErrorInjectionTestSuite, MergePatchAbsentKeyJsonNullFail) { + //Given json_null is injected to fail for an absent member that is + //treated as starting from null (1st recursive frame -> level 1) + json_auto_t* target = loadJson(R"({"a":1})"); + json_auto_t* patch = loadJson(R"({"b":2})"); + ASSERT_NE(nullptr, target); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_null((void*)celix_json_merge_patch, 1, nullptr); + //Then merging should fail + EXPECT_EQ(nullptr, celix_json_merge_patch(target, patch)); +} + +TEST_F(JanssonExtMergePatchErrorInjectionTestSuite, MergePatchSetNewFail) { + //Given json_object_set_new is injected to fail while adding a merged + //member (1st recursive frame -> level 1). The merged value is consumed + //by the injected failure (the wrapper auto-releases it), which LSAN + //verifies. + json_auto_t* target = loadJson("{}"); + json_auto_t* patch = loadJson(R"({"a":1})"); + ASSERT_NE(nullptr, target); + ASSERT_NE(nullptr, patch); + celix_ei_expect_json_object_set_new((void*)celix_json_merge_patch, 1, -1); + //Then merging should fail + EXPECT_EQ(nullptr, celix_json_merge_patch(target, patch)); +} diff --git a/libs/jansson_ext/gtest/src/SchemaErrorInjectionTestSuite.cc b/libs/jansson_ext/gtest/src/SchemaErrorInjectionTestSuite.cc new file mode 100644 index 000000000..aeccbc336 --- /dev/null +++ b/libs/jansson_ext/gtest/src/SchemaErrorInjectionTestSuite.cc @@ -0,0 +1,1766 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include + +#include +#include + +#include "asprintf_ei.h" +#include "celix_cleanup.h" +#include "celix_jansson_pointer.h" +#include "celix_json_patch.h" +#include "celix_jansson_schema.h" +#include "celix_jansson_uri.h" +#include "celix_string_hash_map_ei.h" +#include "celix_util.h" +#include "jansson_ei.h" +#include "malloc_ei.h" +#include "string_ei.h" + +/* celix_schema.h is an internal C header without C++ linkage guards */ +extern "C" { +#include "celix_schema.h" +} + +CELIX_DEFINE_AUTOPTR_CLEANUP_FUNC(celix_jansson_schema_validator_t, celix_jansson_schema_validator_destroy) + +/** + * Error-injection tests for the OOM (out-of-memory) handling paths of + * celix_jansson_schema. + * + * The schema compilation/validation pipelines use several static helpers + * (schema_make_internal_depth, make_type_schema, obj_node_map_create), so the + * injected caller is matched with `level` = the number of frames between the + * wrapped allocator and the exported function that starts the pipeline: + * - make_type_schema callocs: level 2 (make_type_schema <- depth <- set_root_schema) + * - schema_make_internal_depth callocs: level 1 (depth <- set_root_schema) + * - obj_node_map_create createWithOptions: level 3 (obj_node_map_create <- make_type_schema <- depth <- set_root_schema) + * Ordinals only distinguish consecutive allocations from the same caller. + */ +class JanssonExtSchemaErrorInjectionTestSuite : public ::testing::Test { +public: + ~JanssonExtSchemaErrorInjectionTestSuite() noexcept override { + celix_ei_expect_malloc(nullptr, 0, nullptr); + celix_ei_expect_realloc(nullptr, 0, nullptr); + celix_ei_expect_calloc(nullptr, 0, nullptr); + celix_ei_expect_strdup(nullptr, 0, nullptr); + celix_ei_expect_json_deep_copy(nullptr, 0, nullptr); + celix_ei_expect_json_array(nullptr, 0, nullptr); + celix_ei_expect_json_array_append_new(nullptr, 0, 0); + celix_ei_expect_json_string(nullptr, 0, nullptr); + celix_ei_expect_celix_stringHashMap_createWithOptions(nullptr, 0, nullptr); + celix_ei_expect_celix_stringHashMap_put(nullptr, 0, CELIX_ENOMEM); + celix_ei_expect_asprintf(nullptr, 0, 0); + celix_ei_expect_vasprintf(nullptr, 0, 0); + } + +protected: + static celix_jansson_schema_validator_t* makeValidator() { + return celix_jansson_schema_validator_create(nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + } + + static celix_jansson_schema_validator_t* makeValidatorWithLoader() { + return celix_jansson_schema_validator_create(testLoader, nullptr, nullptr, nullptr, nullptr, nullptr); + } + + static json_t* loadSchema(const char* text) { + json_error_t err{}; + return json_loads(text, 0, &err); + } + + /* Loader used by the external-ref tests. The returned document registers + * both a definitions entry (resolved directly after loading) and a + * properties entry (only reachable via the document-fragment walk). */ + static int testLoader(const char* location, json_t** out, void* ud) { + (void)ud; + if (strcmp(location, "http://example.com/doc") != 0) + return CELIX_JANSSON_SCHEMA_ERROR_LOADER; + *out = json_loads( + "{\"definitions\":{\"x\":{\"type\":\"string\"}},\"properties\":{\"b\":{\"type\":\"string\"}}}", + 0, + nullptr); + return *out ? CELIX_JANSSON_SCHEMA_OK : CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } +}; + +static void countingErrorCb(const char*, json_t*, const char*, void* ud) { + int* count = static_cast(ud); + (*count)++; +} + +static void captureMessageCb(const char*, json_t*, const char* msg, void* ud) { + std::string* out = static_cast(ud); + *out = msg ? msg : ""; +} + +/* ── path_push ────────────────────────────────────────────────────────── */ + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaPathPushReallocFail) { + //Given realloc is injected to fail in path_push + celix_jansson_path_t p; + celix_jansson_path_init(&p); + celix_ei_expect_realloc((void*)celix_jansson_path_push, 0, nullptr); + //Then pushing a path token should fail + EXPECT_EQ(-1, celix_jansson_path_push(&p, "abc")); + celix_jansson_path_free(&p); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaPathPushStrdupFail) { + //Given strdup is injected to fail in path_push + celix_jansson_path_t p; + celix_jansson_path_init(&p); + celix_ei_expect_strdup((void*)celix_jansson_path_push, 0, nullptr); + //Then pushing a path token should fail + EXPECT_EQ(-1, celix_jansson_path_push(&p, "abc")); + celix_jansson_path_free(&p); +} + +/* ── Schema compilation (set_root_schema) ─────────────────────────────── */ + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaBooleanNodeCallocFail) { + //Given a validator and calloc is injected to fail for the boolean node + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + celix_ei_expect_calloc((void*)celix_jansson_schema_set_root_schema, 1, nullptr); + //Then compiling a boolean schema should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, json_true(), nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaNodeCallocFail) { + //Given a validator and calloc is injected to fail for the type-schema node + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"string\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_set_root_schema, 2, nullptr); + //Then compiling the schema should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaTypedNodeCallocFail) { + //Given calloc is injected to fail for the per-type (typed) node + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"string\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_set_root_schema, 2, nullptr, 2); + //Then compiling the schema should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaNoTypeSlotCallocFail) { + //Given calloc is injected to fail for the first 7-slot node (schema without "type") + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_set_root_schema, 2, nullptr, 2); + //Then compiling the schema should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaTypeArrayNonString) { + //Given a "type" array containing a non-string entry (no injection needed) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":[1,\"string\"]}"); + ASSERT_NE(nullptr, schema); + //Then compiling should skip the non-string entry and succeed + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaPropertiesMapCreateFail) { + //Given stringHashMap creation is injected to fail for the properties map + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"properties\":{\"a\":{\"type\":\"string\"}}}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_celix_stringHashMap_createWithOptions((void*)celix_jansson_schema_set_root_schema, 3, nullptr); + //Then compiling the schema should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaDependenciesMapCreateFail) { + //Given stringHashMap creation is injected to fail for the dependencies map + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"dependencies\":{\"a\":{\"type\":\"string\"}}}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_celix_stringHashMap_createWithOptions((void*)celix_jansson_schema_set_root_schema, 3, nullptr); + //Then compiling the schema should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaRefUriDeriveReallocFail) { + //Given realloc is injected to fail in strbuf_append (used by percent_decode during $ref URI + //derivation; realloc <- strbuf_append <- appendc <- percent_decode, so level 0). The + //single-char fragment "#x" makes percent_decode loop exactly once, so the single injected + //realloc failure is not recovered by a later iteration. + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema( + "{\"$id\":\"http://example.com/root.json\",\"properties\":{\"p\":{\"$ref\":\"#x\"}}}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_realloc((void*)celix_jansson_strbuf_append, 0, nullptr); + //Then the $ref URI derivation fails with NOMEM, which is now propagated + //through the properties subschema compilation (make_type_schema). + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaRefTargetPlaceholderCallocFail) { + //Given calloc is injected to fail for the unresolved $ref target placeholder + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$ref\":\"#/nonexistent\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_set_root_schema, 1, nullptr); + //Then compiling the schema should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaRefNodeCallocFail) { + //Given calloc is injected to fail for the $ref node (second allocation: placeholder succeeds) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$ref\":\"#\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_set_root_schema, 1, nullptr, 2); + //Then compiling the schema should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaRefLocationOomFail) { + //Given strdup is injected to fail inside uri_location during $ref compilation. + //The schema has no $id, so the first uri_location call is for the $ref URI; + //today its NULL return reaches get_or_create_file and crashes the hashmap. + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$ref\":\"urn:foo\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_strdup((void*)celix_jansson_uri_location, 0, nullptr); + //Then compiling the schema should fail with NOMEM instead of crashing + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaRootInsertLocationOomFail) { + //Given strdup is injected to fail inside uri_location during root_insert. + //The first uri_location call in this flow is root_insert's; it also + //exercises the root->root cleanup path of set_root_schema. + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"string\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_strdup((void*)celix_jansson_uri_location, 0, nullptr); + //Then compiling the schema should fail with NOMEM instead of crashing + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaGetOrCreateFileCallocFail) { + //Given calloc is injected to fail in get_or_create_file (first call, from root_insert) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"string\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_root_get_or_create_file, 0, nullptr); + //Then get_or_create_file returns NULL, root_insert reports NOMEM and + //set_root_schema now propagates it (with an error message) + char* errmsg = nullptr; + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, &errmsg)); + EXPECT_NE(nullptr, errmsg); + free(errmsg); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaGetOrCreateFileMapsFail) { + //Given stringHashMap creation is injected to fail in get_or_create_file (via static obj_node_map_create) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"string\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_celix_stringHashMap_createWithOptions((void*)celix_jansson_schema_root_get_or_create_file, 1, nullptr); + //Then get_or_create_file returns NULL, root_insert reports NOMEM and + //set_root_schema now propagates it. + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaSetRootSchemaDeepCopyFail) { + //Given json_deep_copy is injected to fail in set_root_schema + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"string\"}"); + ASSERT_NE(nullptr, schema); + char* errmsg = nullptr; + celix_ei_expect_json_deep_copy((void*)celix_jansson_schema_set_root_schema, 0, nullptr); + //Then compiling the schema should fail with NOMEM and set an error message + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, &errmsg)); + EXPECT_NE(nullptr, errmsg); + free(errmsg); +} + +/* ── Validator creation ───────────────────────────────────────────────── */ + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidatorCreateCallocFail) { + //Given calloc is injected to fail for the validator struct + celix_ei_expect_calloc((void*)celix_jansson_schema_validator_create, 0, nullptr); + //Then creating a validator should fail + EXPECT_EQ(nullptr, makeValidator()); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidatorCreateRootCallocFail) { + //Given calloc is injected to fail for the root struct (second calloc) + celix_ei_expect_calloc((void*)celix_jansson_schema_validator_create, 0, nullptr, 2); + //Then creating a validator should fail + EXPECT_EQ(nullptr, makeValidator()); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidatorCreateFilesMapFail) { + //Given stringHashMap creation is injected to fail for root->files + celix_ei_expect_celix_stringHashMap_createWithOptions((void*)celix_jansson_schema_validator_create, 0, nullptr); + //Then creating a validator should fail + EXPECT_EQ(nullptr, makeValidator()); +} + +/* ── Validation ───────────────────────────────────────────────────────── */ + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateSinkCallocFail) { + //Given a validator with a compiled schema + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"string\"}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + //And calloc is injected to fail for the user sink + celix_ei_expect_calloc((void*)celix_jansson_schema_validate, 0, nullptr); + //Then validating should fail + EXPECT_EQ(-1, celix_jansson_schema_validate(v, json_true(), nullptr, nullptr, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateUriSinkCallocFail) { + //Given a validator with a compiled schema + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"string\"}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + //And calloc is injected to fail for the user sink + celix_ei_expect_calloc((void*)celix_jansson_schema_validate_uri, 0, nullptr); + //Then validating by URI should fail + EXPECT_EQ(-1, celix_jansson_schema_validate_uri(v, json_true(), "#", nullptr, nullptr, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaErrorListAddReallocFail) { + //Given a validator with an anyOf schema + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"anyOf\":[{\"type\":\"string\"}]}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + //And realloc is injected to fail in error_list_add (branch errors are silently dropped) + int errorCount = 0; + celix_ei_expect_realloc((void*)celix_jansson_error_list_add, 0, nullptr); + //When validating an integer against the anyOf(string) schema + json_auto_t* instance = json_integer(42); + int errs = celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr); + //Then the branch errors are dropped and only the anyOf error is reported + EXPECT_EQ(1, errs); + EXPECT_EQ(1, errorCount); +} + +/* ── OOM fixes: unchecked callocs in make_type_schema ─────────────────── */ + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaTypeArrayTypedCallocFail) { + //Given calloc is injected to fail for the per-type node of the "type" array form + //(1st calloc = the type node, 2nd = the typed node) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":[\"string\"]}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_set_root_schema, 2, nullptr, 2); + //Then compiling the schema should fail with NOMEM instead of crashing + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaRequiredArrayCallocFail) { + //Given calloc is injected to fail for the required array + //(1st = type node, 2nd = typed object node, 3rd = required array) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"object\",\"required\":[\"a\"]}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_set_root_schema, 2, nullptr, 3); + //Then compiling the schema should fail with NOMEM instead of crashing + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaPatternPropertiesCallocFail) { + //Given calloc is injected to fail for the patternProperties array + //(1st = type node, 2nd = typed object node, 3rd = patternProperties array) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"object\",\"patternProperties\":{\"^a\":{}}}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_set_root_schema, 2, nullptr, 3); + //Then compiling the schema should fail with NOMEM instead of crashing + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaDefinitionsGetOrCreateFileCallocFail) { + //Given calloc is injected to fail in get_or_create_file, triggered by the + //definitions block (the first get_or_create_file call during compilation) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"definitions\":{\"a\":{}}}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_root_get_or_create_file, 0, nullptr); + //Then compiling the schema should fail with NOMEM instead of crashing + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +/* ── OOM fixes: DUPLICATE_URI tolerance and error propagation ─────────── */ + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaRootIdEmptyDuplicateIgnored) { + //Given a schema with an empty top-level $id (registered during make with the + //"" key) and no injection + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$id\":\"\",\"type\":\"string\"}"); + ASSERT_NE(nullptr, schema); + //Then the second registration in set_root_schema returns DUPLICATE_URI, + //which must be tolerated + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaPropertiesInvalidPatternPropagates) { + //Given a properties subschema whose pattern fails to compile (regcomp) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"properties\":{\"x\":{\"type\":\"string\",\"pattern\":\"***invalid\"}}}"); + ASSERT_NE(nullptr, schema); + char* errmsg = nullptr; + //Then the compile error is propagated from the subschema + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_PATTERN, + celix_jansson_schema_set_root_schema(v, schema, &errmsg)); + EXPECT_NE(nullptr, errmsg); + free(errmsg); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaOriginalSchemaDeepCopyFail) { + //Given json_deep_copy is injected to fail for the second copy (the original + //schema retention); the first copy succeeds (ordinal 1) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"string\"}"); + ASSERT_NE(nullptr, schema); + char* errmsg = nullptr; + celix_ei_expect_json_deep_copy((void*)celix_jansson_schema_set_root_schema, 0, nullptr, 2); + //Then compiling the schema should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, &errmsg)); + EXPECT_NE(nullptr, errmsg); + free(errmsg); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaRootIdInsertFileCallocFail) { + //Given calloc is injected to fail in get_or_create_file, reached through the + //top-level $id registration in schema_make_internal_depth + //(calloc <- get_or_create_file <- root_insert <- schema_make_internal_depth) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$id\":\"http://example.com/x\",\"type\":\"string\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_root_insert, 1, nullptr); + //Then compiling the schema should fail with NOMEM (registration propagated) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +/* ── OOM fixes: remaining unchecked callocs (dependencies/items/not/combos) ── */ + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaDependenciesArrayCallocFail) { + //Given calloc is injected to fail for the dependencies array-form node + //(1st = type node, 2nd = typed object node, 3rd = the required-list node) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"object\",\"dependencies\":{\"a\":[\"x\"]}}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_set_root_schema, 2, nullptr, 3); + //Then compiling the schema should fail with NOMEM instead of crashing + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaDependenciesArrayNamesCallocFail) { + //Given calloc is injected to fail for the required-names array of a dependencies + //array-form node (1st = type node, 2nd = typed object node, 3rd = required-list + //node, 4th = names array) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"object\",\"dependencies\":{\"a\":[\"x\"]}}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_set_root_schema, 2, nullptr, 4); + //Then compiling the schema should fail with NOMEM instead of crashing + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaItemsTupleCallocFail) { + //Given calloc is injected to fail for the tuple-form items array + //(1st = type node, 2nd = typed array node, 3rd = items array) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"array\",\"items\":[{\"type\":\"string\"}]}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_set_root_schema, 2, nullptr, 3); + //Then compiling the schema should fail with NOMEM instead of crashing + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaNotNodeCallocFail) { + //Given calloc is injected to fail for the "not" node + //(1st = type node, 2nd = typed object node, 3rd = not node; the subschema's + //own callocs do not match this caller) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"object\",\"not\":{\"type\":\"string\"}}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_set_root_schema, 2, nullptr, 3); + //Then compiling the schema should fail with NOMEM instead of crashing + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaComboNodeCallocFail) { + //Given calloc is injected to fail for the allOf node + //(1st = type node, 2nd = typed object node, 3rd = combo node) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"object\",\"allOf\":[{\"type\":\"string\"}]}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_set_root_schema, 2, nullptr, 3); + //Then compiling the schema should fail with NOMEM instead of crashing + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaComboItemsCallocFail) { + //Given calloc is injected to fail for the combo items array + //(1st = type node, 2nd = typed object node, 3rd = combo node, 4th = items array) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"object\",\"allOf\":[{\"type\":\"string\"}]}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_set_root_schema, 2, nullptr, 4); + //Then compiling the schema should fail with NOMEM instead of crashing + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaComboSubschemaFailWithNot) { + //Given a schema with a "not" node already pushed to the logic vec and an allOf + //whose subschema fails to compile (no injection needed) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"object\",\"not\":{},\"allOf\":[1]}"); + ASSERT_NE(nullptr, schema); + //Then the allOf compile error is propagated and the not node is released + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_SCHEMA, + celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaComboNodeCallocFailWithNot) { + //Given calloc is injected to fail for the combo node while the logic vec + //already holds a "not" node (1st = type node, 2nd = typed object node, + //3rd = not node, 4th = combo node) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"object\",\"not\":{},\"allOf\":[{\"type\":\"string\"}]}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_set_root_schema, 2, nullptr, 4); + //Then compiling the schema should fail with NOMEM and release the not node + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaComboItemsCallocFailWithNot) { + //Given calloc is injected to fail for the combo items array while the logic + //vec already holds a "not" node (1st = type node, 2nd = typed object node, + //3rd = not node, 4th = combo node, 5th = items array) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"object\",\"not\":{},\"allOf\":[{\"type\":\"string\"}]}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_set_root_schema, 2, nullptr, 5); + //Then compiling the schema should fail with NOMEM and release the not node + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +/* ── Remaining defensive branches and internal API NULL handling ───────── */ + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, VCombCollectingSinkAllocFail) { + //Given a validator with an allOf schema + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"allOf\":[{\"type\":\"integer\"}]}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + //And calloc is injected to fail for the master collecting sink + //(calloc <- coll_new <- v_comb <- v_type <- root_validate, so level 3) + json_auto_t* instance = json_integer(5); + int errorCount = 0; + celix_ei_expect_calloc((void*)celix_jansson_schema_root_validate, 3, nullptr, 1); + //Then validating should report the combination as failed (fail-closed) + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); + + //And calloc is injected to fail for the first branch collecting sink + errorCount = 0; + celix_ei_expect_calloc((void*)celix_jansson_schema_root_validate, 3, nullptr, 2); + //Then validating should report the combination as failed as well + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, RootDestroyNullIsNoOp) { + //Given a NULL root (no injection needed) + //Then destroying it should be a no-op + celix_jansson_schema_root_destroy(nullptr); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, RootValidateNullArgs) { + //Given a NULL root + //Then validating should return -1 + EXPECT_EQ(-1, celix_jansson_schema_root_validate(nullptr, "#", nullptr, nullptr)); + //And with a zero-initialized root but NULL context it should also return -1 + //(the ctx check fires before any root member is touched) + celix_jansson_schema_root_t r = {}; + EXPECT_EQ(-1, celix_jansson_schema_root_validate(&r, "#", nullptr, nullptr)); +} + +/* ── OOM fixes: $id registration and document resolution ───────────────── */ + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaNestedDefinitionsGetOrCreateFileCallocFail) { + //Given calloc is injected to fail in get_or_create_file, reached through the + //definitions block of a nested schema with its own $id + //(exercises the my_base cleanup when id_stored_in_out is false) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = + loadSchema("{\"properties\":{\"p\":{\"$id\":\"http://example.com/sub\",\"definitions\":{\"a\":{}}}}}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_root_get_or_create_file, 0, nullptr); + //Then compiling the schema should fail with NOMEM (propagated through properties) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaNestedIdInsertFileCallocFail) { + //Given calloc is injected to fail in get_or_create_file, reached through the + //$id registration of a nested schema (id_stored_in_out is false → my_base + //must be cleared on failure) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"properties\":{\"p\":{\"$id\":\"http://example.com/sub\",\"type\":\"string\"}}}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_calloc((void*)celix_jansson_schema_root_insert, 1, nullptr); + //Then compiling the schema should fail with NOMEM (registration propagated) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +/* ── Remaining OOM branches of schema_make_internal_depth and the + * document-fragment resolver (reached via error injection) ──────────── */ + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaDefinitionsBaseLocOomFail) { + //Given realloc is injected to fail in strbuf_append, hit by the definitions + //block of a nested schema with its own $id (realloc <- strbuf_append <- + //strbuf_appends <- uri_location, so level 0). The nested schema is compiled + //with eff_base_out == NULL (schema_make_internal), so base_loc comes from + //uri_location(my_base) and the failure must also clear my_base. + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = + loadSchema("{\"properties\":{\"p\":{\"$id\":\"http://x/nested\",\"definitions\":{\"x\":{\"type\":\"string\"}}}}}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_realloc((void*)celix_jansson_strbuf_append, 0, nullptr); + //Then compiling the schema should fail with NOMEM + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaRefNoBaseUriInitOomFail) { + //Given malloc is injected to fail inside uri_update while parsing the $ref + //URI with no effective base (root schema without $id). The first + //uri_update allocation is the location buffer of the ref URI. + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$ref\":\"x\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_malloc((void*)celix_jansson_uri_update, 0, nullptr); + //Then compiling the schema should fail with NOMEM instead of crashing + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaRefPlaceholderToStringOomFail) { + //Given strdup is injected to fail inside uri_location, hit by the second + //empty-location strdup: the placeholder node's uri_to_string during $ref + //compilation (1st = rloc, 2nd = uri_to_string's location). + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$ref\":\"#/definitions/missing\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_strdup((void*)celix_jansson_uri_location, 0, nullptr, 2); + //Then compiling the schema should fail with NOMEM instead of crashing + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaRefNodeToStringOomFail) { + //Given strdup is injected to fail inside uri_location, hit by the second + //empty-location strdup: the $ref node's uri_to_string when the target is + //already registered (1st = rloc, 2nd = uri_to_string's location). + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = + loadSchema("{\"definitions\":{\"x\":{\"type\":\"string\"}},\"$ref\":\"#/definitions/x\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_strdup((void*)celix_jansson_uri_location, 0, nullptr, 2); + //Then compiling the schema should fail with NOMEM instead of crashing + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaDocFragmentCurBaseUriOomFail) { + //Given strdup is injected to fail in uri_update, hit by the document-fragment + //walk's cur_base init in resolve_document_fragment (1st = root $ref path, + //2nd = retrieval URI path, 3rd = cur_base path). The walk returns NULL and + //the ref is resolved on the next resolve_external_refs iteration. + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidatorWithLoader(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$ref\":\"http://example.com/doc#/properties/b\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_strdup((void*)celix_jansson_uri_update, 0, nullptr, 3); + //Then the schema still compiles (the failed walk self-heals on retry) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaDocFragmentNoBaseUriOomFail) { + //Given strdup is injected to fail inside compile_external_document, keeping + //the external file's base_uri NULL. The 2nd frame-2 strdup matches (1st = + //root_insert's fragment strdup, 2nd = strdup(location) in + //compile_external_document). And malloc is injected to fail in uri_update, + //hit by the walk's cur_base init taken from the location instead of + //base_uri (10th uri_update malloc: 3 for the root $id derive, 3 for the + //$ref URI, 3 for the retrieval URI, then cur_base). + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidatorWithLoader(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema( + "{\"$id\":\"http://x/root\",\"properties\":{\"p\":{\"$ref\":\"http://example.com/doc#/properties/b\"}}}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_strdup((void*)celix_jansson_schema_set_root_schema, 2, nullptr, 2); + celix_ei_expect_malloc((void*)celix_jansson_uri_update, 0, nullptr, 10); + //Then the schema still compiles (the failed walk self-heals on retry) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaDocFragmentFullUriOomFail) { + //Given strdup is injected to fail in uri_update, hit by the walk's full_uri + //init in resolve_document_fragment (1st = root $ref path, 2nd = retrieval + //URI path, 3rd = cur_base path, 4th = full_uri path). + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidatorWithLoader(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$ref\":\"http://example.com/doc#/properties/b\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_strdup((void*)celix_jansson_uri_update, 0, nullptr, 4); + //Then the schema still compiles (the failed walk self-heals on retry) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaDocFragmentPointerPushOomFail) { + //Given strdup is injected to fail in celix_json_pointer_push, hit by the + //walk's fragment re-attach loop. The push is also used internally by + //pointer_init (1st-2nd = $ref URI pointer_init, 3rd-4th = walk pointer_init, + //5th = re-attach push). The walk returns NULL and the ref is resolved on + //the next resolve_external_refs iteration. + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = + loadSchema("{\"properties\":{\"b\":{\"type\":\"string\"}},\"$ref\":\"#/properties/b\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_strdup((void*)celix_json_pointer_push, 0, nullptr, 5); + //Then the schema still compiles (the failed walk self-heals on retry) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaCompileExternalUriInitOomFail) { + //Given strdup is injected to fail in uri_update, hit by the retrieval-URI + //init in compile_external_document (1st = root $ref path, 2nd = retrieval + //URI path). The loader-based document compile fails and propagates NOMEM. + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidatorWithLoader(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$ref\":\"http://example.com/doc#/definitions/x\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_strdup((void*)celix_jansson_uri_update, 0, nullptr, 2); + //Then compiling the schema should fail with NOMEM instead of crashing + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaResolveExternalPhaseAvecOomFail) { + //Given realloc is injected to fail in celix_jansson_vec_push, hit by the + //Phase A location-key snapshot of resolve_external_refs (1st vec_push). + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidatorWithLoader(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$ref\":\"http://example.com/doc#/definitions/x\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_realloc((void*)celix_jansson_vec_push, 0, nullptr); + //Then compiling the schema should fail with NOMEM instead of crashing + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaResolveExternalPhaseBPairAllocOomFail) { + //Given malloc is injected to fail for the Phase B (location, fragment) pair + //allocation of resolve_external_refs (1st frame-1 malloc of set_root_schema). + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidatorWithLoader(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$ref\":\"http://example.com/doc#/definitions/x\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_malloc((void*)celix_jansson_schema_set_root_schema, 1, nullptr); + //Then the schema still compiles (the failed snapshot self-heals on retry) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaResolveExternalPhaseBPairVecPushOomFail) { + //Given realloc is injected to fail in celix_jansson_vec_push, hit by the + //Phase B pairs-vec push (1st realloc = Phase A locs-vec push, 2nd = pairs + //push, which triggers the pair cleanup). + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidatorWithLoader(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$ref\":\"http://example.com/doc#/definitions/x\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_realloc((void*)celix_jansson_vec_push, 0, nullptr, 2); + //Then the schema still compiles (the failed snapshot self-heals on retry) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaResolveExternalRegisterRootOomFail) { + //Given realloc is injected to fail inside root_insert's uri_location + //while registering the external document at its retrieval URI (realloc <- + //strbuf_append <- strbuf_appends <- uri_location <- root_insert, so level 3) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidatorWithLoader(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$ref\":\"http://example.com/doc\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_realloc((void*)celix_jansson_schema_root_insert, 3, nullptr); + //Then compiling fails with NOMEM and the document/node refs are released + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaResolveExternalFragmentRegisterOomFail) { + //Given realloc is injected to fail inside root_insert's uri_location + //while the document-fragment walk registers the resolved node (ordinal 2: + //the external document itself is registered first) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidatorWithLoader(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$ref\":\"http://example.com/doc#/definitions/x\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_realloc((void*)celix_jansson_schema_root_insert, 3, nullptr, 2); + //Then the fragment-walk registration fails and the schema still compiles + //(the placeholder resolution retries on the next iteration) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaResolveExternalPhaseAKeyStrdupOomFail) { + //Given strdup is injected to fail for the Phase A location-key snapshot of + //resolve_external_refs. The 3rd frame-1 strdup matches (1st = the + //set_root_schema rloc-block uri_location strdup, which is tolerated, 2nd = + //first file key, 3rd = second file key — failing it exercises the snapshot + //cleanup loop over the already-collected keys). + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidatorWithLoader(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$ref\":\"http://example.com/doc#/definitions/x\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_strdup((void*)celix_jansson_schema_set_root_schema, 1, nullptr, 3); + //Then compiling the schema should fail with NOMEM instead of crashing + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +/* ── Runtime validator OOM: first-sink callocs (P0) ───────────────────── */ + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateAdditionalPropertiesSinkOomFail) { + //Given a validator with an object schema that rejects additional properties + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"object\",\"additionalProperties\":{\"type\":\"string\"}}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + //And calloc is injected to fail for the additionalProperties first-sink + //(calloc <- first_sink_new <- v_object <- v_type <- root_validate, so level 3) + json_auto_t* instance = loadSchema("{\"a\":\"x\"}"); + int errorCount = 0; + celix_ei_expect_calloc((void*)celix_jansson_schema_root_validate, 3, nullptr); + //Then validating fails closed with an out-of-memory error instead of crashing + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateContainsSinkOomFail) { + //Given a validator with a contains schema + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"array\",\"contains\":{\"type\":\"string\"}}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + //And calloc is injected to fail for the contains first-sink + //(calloc <- first_sink_new <- v_array <- v_type <- root_validate, so level 3) + json_auto_t* instance = json_pack("[s]", "x"); + int errorCount = 0; + celix_ei_expect_calloc((void*)celix_jansson_schema_root_validate, 3, nullptr); + //Then validating fails closed with an out-of-memory error instead of crashing + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateNotSinkOomFail) { + //Given a validator with a not schema + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"not\":{\"type\":\"string\"}}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + //And calloc is injected to fail for the not first-sink + //(calloc <- first_sink_new <- v_not <- v_type <- root_validate, so level 3) + json_auto_t* instance = json_integer(42); + int errorCount = 0; + celix_ei_expect_calloc((void*)celix_jansson_schema_root_validate, 3, nullptr); + //Then validating fails closed with an out-of-memory error instead of crashing + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateIfSinkOomFail) { + //Given a validator with an if/then schema + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"if\":{\"type\":\"string\"},\"then\":{\"type\":\"integer\"}}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + //And calloc is injected to fail for the if-condition first-sink + //(calloc <- first_sink_new <- v_type <- root_validate, so level 2) + json_auto_t* instance = json_integer(42); + int errorCount = 0; + celix_ei_expect_calloc((void*)celix_jansson_schema_root_validate, 2, nullptr); + //Then validating fails closed with an out-of-memory error instead of crashing + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidatePropertyNamesJsonStringOomFail) { + //Given a validator with a propertyNames schema + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"object\",\"propertyNames\":{\"type\":\"string\"}}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + //And json_string is injected to fail for the property-name wrapper + //(json_string <- v_object <- v_type <- root_validate, so level 2) + json_auto_t* instance = loadSchema("{\"a\":1}"); + int errorCount = 0; + celix_ei_expect_json_string((void*)celix_jansson_schema_root_validate, 2, nullptr); + //Then validating fails closed with an out-of-memory error instead of + //crashing on json_typeof(NULL) + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +/* ── Runtime validator OOM: patch array / error list / path building ──── */ + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidatePatchArrayOomFail) { + //Given a validator with a compiled schema + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"string\"}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + //And json_array is injected to fail for the patch + json_auto_t* instance = json_string("x"); + celix_ei_expect_json_array((void*)celix_jansson_schema_validate, 0, nullptr); + //Then validating should fail instead of running with a broken patch + EXPECT_EQ(-1, celix_jansson_schema_validate(v, instance, nullptr, nullptr, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateUriPatchArrayOomFail) { + //Given a validator with a compiled schema + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"string\"}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + //And json_array is injected to fail for the patch + json_auto_t* instance = json_string("x"); + celix_ei_expect_json_array((void*)celix_jansson_schema_validate_uri, 0, nullptr); + //Then validating by URI should fail instead of running with a broken patch + EXPECT_EQ(-1, celix_jansson_schema_validate_uri(v, instance, "#", nullptr, nullptr, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateErrorListStrdupOomFail) { + //Given a validator with an anyOf schema + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"anyOf\":[{\"type\":\"string\"}]}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + //And strdup is injected to fail for the error entry's path (message survives) + int errorCount = 0; + celix_ei_expect_strdup((void*)celix_jansson_error_list_add, 0, nullptr); + //When validating an integer against the anyOf(string) schema + json_auto_t* instance = json_integer(42); + int errs = celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr); + //Then the branch error is still propagated with an empty path (no crash on + //appends(NULL)/emit(NULL)) and the anyOf error is reported as well + EXPECT_EQ(1, errs); + EXPECT_EQ(2, errorCount); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidatePathChildOomFail) { + //Given a validator with an object schema + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"object\",\"properties\":{\"a\":{\"type\":\"string\"}}}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + //And realloc is injected to fail inside path_child_checked + //(realloc <- path_push <- path_child_checked <- v_object <- v_type <- root_validate, so level 4) + json_auto_t* instance = loadSchema("{\"a\":\"x\"}"); + int errorCount = 0; + celix_ei_expect_realloc((void*)celix_jansson_schema_root_validate, 4, nullptr); + //Then validating fails closed with an out-of-memory error instead of + //silently descending with a truncated path + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +/* ── Compile-time OOM: strdup / deep_copy propagation (P1) ────────────── */ +/* The root $id keeps root_insert's uri_location/fragment as strbuf reallocs + * (no strdup), so the level-2 strdup ordinals below count only + * make_type_schema's own strdups plus root_insert's fragment strdup(""). */ + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaPatternStrdupOomFail) { + //Given strdup is injected to fail for the pattern string + //(1st level-2 strdup of make_type_schema, before root_insert's fragment strdup) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$id\":\"http://x/root\",\"type\":\"string\",\"pattern\":\"a\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_strdup((void*)celix_jansson_schema_set_root_schema, 2, nullptr); + //Then compiling the schema should fail with NOMEM instead of compiling a + //schema whose pattern_str is NULL + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaFormatStrdupOomFail) { + //Given strdup is injected to fail for the format string + //(1st level-2 strdup; the missing format checker returns early, so this + //is the only strdup in the flow) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$id\":\"http://x/root\",\"type\":\"string\",\"format\":\"email\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_strdup((void*)celix_jansson_schema_set_root_schema, 2, nullptr); + //Then compiling the schema should fail with NOMEM instead of passing NULL + //to a format checker at validation time + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaContentEncodingStrdupOomFail) { + //Given strdup is injected to fail for the contentEncoding string + //(1st level-2 strdup; the missing content checker returns early, so this + //is the only strdup in the flow) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$id\":\"http://x/root\",\"type\":\"string\",\"contentEncoding\":\"base64\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_strdup((void*)celix_jansson_schema_set_root_schema, 2, nullptr); + //Then compiling the schema should fail with NOMEM instead of passing NULL + //to a content checker at validation time + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaRequiredStrdupOomFail) { + //Given strdup is injected to fail for the second required property name + //(2nd level-2 strdup: 1st = "a", later ones = root_insert's fragment strdup("")) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$id\":\"http://x/root\",\"type\":\"object\",\"required\":[\"a\",\"b\"]}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_strdup((void*)celix_jansson_schema_set_root_schema, 2, nullptr, 2); + //Then compiling the schema should fail with NOMEM instead of storing a + //NULL property name + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaDependenciesRequiredStrdupOomFail) { + //Given strdup is injected to fail for the second dependency name + //(2nd level-2 strdup: 1st = "x", later ones = root_insert's fragment strdup("")) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$id\":\"http://x/root\",\"type\":\"object\",\"dependencies\":{\"a\":[\"x\",\"y\"]}}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_strdup((void*)celix_jansson_schema_set_root_schema, 2, nullptr, 2); + //Then compiling the schema should fail with NOMEM instead of storing a + //NULL dependency name + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaEnumDeepCopyOomFail) { + //Given json_deep_copy is injected to fail for the enum values + //(1st level-2 deep_copy: set_root_schema's two copies are level 1) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$id\":\"http://x/root\",\"type\":\"string\",\"enum\":[\"a\"]}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_json_deep_copy((void*)celix_jansson_schema_set_root_schema, 2, nullptr); + //Then compiling the schema should fail with NOMEM instead of validating + //every instance against a NULL enum + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaConstDeepCopyOomFail) { + //Given json_deep_copy is injected to fail for the const value + //(1st level-2 deep_copy: set_root_schema's two copies are level 1) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$id\":\"http://x/root\",\"type\":\"string\",\"const\":\"a\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_json_deep_copy((void*)celix_jansson_schema_set_root_schema, 2, nullptr); + //Then compiling the schema should fail with NOMEM instead of validating + //every instance against a NULL const + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +/* ── Registry OOM: stringHashMap_put failures (P2) ────────────────────── */ + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaPropertiesPutOomFail) { + //Given stringHashMap_put is injected to fail for a properties entry + //(put <- make_type_schema <- depth <- set_root_schema, so level 2) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$id\":\"http://x/root\",\"type\":\"object\",\"properties\":{\"a\":{\"type\":\"string\"}}}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_celix_stringHashMap_put((void*)celix_jansson_schema_set_root_schema, 2, CELIX_ENOMEM); + //Then compiling the schema should fail with NOMEM instead of leaking the + //compiled subschema + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaDependenciesPutOomFail) { + //Given stringHashMap_put is injected to fail for a dependencies entry + //(put <- make_type_schema <- depth <- set_root_schema, so level 2) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$id\":\"http://x/root\",\"type\":\"object\",\"dependencies\":{\"a\":{\"type\":\"string\"}}}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_celix_stringHashMap_put((void*)celix_jansson_schema_set_root_schema, 2, CELIX_ENOMEM); + //Then compiling the schema should fail with NOMEM instead of leaking the + //compiled dependency + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaDefinitionsPutOomFail) { + //Given stringHashMap_put is injected to fail for a definitions entry + //(put <- depth <- set_root_schema, so level 1) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"definitions\":{\"x\":{\"type\":\"string\"}},\"type\":\"string\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_celix_stringHashMap_put((void*)celix_jansson_schema_set_root_schema, 1, CELIX_ENOMEM); + //Then compiling the schema should fail with NOMEM instead of leaking the + //compiled definition + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaDefinitionsFragAsprintfFail) { + //Given asprintf is injected to fail for the definitions fragment key + //(asprintf <- depth <- set_root_schema, so level 1) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"definitions\":{\"a\":{\"type\":\"string\"}}}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_asprintf((void*)celix_jansson_schema_set_root_schema, 1, -1); + //Then compiling fails with NOMEM and the definition node is released + //(no leak: the map never took the ref) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaUnresolvedPutOomFail) { + //Given stringHashMap_put is injected to fail for the unresolved-ref + //placeholder entry (put <- depth <- set_root_schema, so level 1) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$ref\":\"#/definitions/x\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_celix_stringHashMap_put((void*)celix_jansson_schema_set_root_schema, 1, CELIX_ENOMEM); + //Then compiling the schema should fail with NOMEM instead of leaving the + //placeholder's owning ref stranded + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaGetOrCreateFilePutOomFail) { + //Given stringHashMap_put is injected to fail inside get_or_create_file + //(put <- get_or_create_file <- root_insert <- set_root_schema, so level 2) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"string\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_celix_stringHashMap_put((void*)celix_jansson_schema_set_root_schema, 2, CELIX_ENOMEM); + //Then get_or_create_file returns NULL, root_insert reports NOMEM and + //set_root_schema propagates it instead of registering a file the map does + //not contain + char* errmsg = nullptr; + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, &errmsg)); + EXPECT_NE(nullptr, errmsg); + free(errmsg); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaRootInsertPutOomFail) { + //Given stringHashMap_put is injected to fail for the root node registration + //in root_insert (put <- root_insert <- depth <- set_root_schema, so level 2; + //the get_or_create_file put inside root_insert is level 3) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$id\":\"http://x/root\",\"type\":\"string\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_celix_stringHashMap_put((void*)celix_jansson_schema_set_root_schema, 2, CELIX_ENOMEM); + //Then compiling the schema should fail with NOMEM instead of reporting OK + //for a node the registry never stored (which would later surface as a + //confusing REF_UNRESOLVED) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaRootInsertRetainedVecPushOomFail) { + //Given realloc is injected to fail in celix_jansson_vec_push, hit by the + //retained-placeholder push inside root_insert (1st = Phase A locs-vec push, + //2nd = Phase B pairs push, 3rd = retained push during the document-fragment + //walk's root_insert). The failed walk is retried on the next + //resolve_external_refs iteration. + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidatorWithLoader(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$ref\":\"http://example.com/doc#/properties/b\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_realloc((void*)celix_jansson_vec_push, 0, nullptr, 3); + //Then the schema still compiles (the failed resolution self-heals on retry) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaResolvePlaceholderVecPushOomFail) { + //Given realloc is injected to fail in celix_jansson_vec_push, hit by the + //retained-placeholder push inside resolve_placeholder (1st = Phase A + //locs-vec push, 2nd = Phase B pairs push, 3rd = retained push for the + //definitions entry registered by the document compile). The failed + //resolution is retried on the next resolve_external_refs iteration. + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidatorWithLoader(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$ref\":\"http://example.com/doc#/definitions/x\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_realloc((void*)celix_jansson_vec_push, 0, nullptr, 3); + //Then the schema still compiles (the failed resolution self-heals on retry) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +/* ── Consistency: patternProperties regcomp (P3) ───────────────────────── */ + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaPatternPropertiesInvalidRegcomp) { + //Given a patternProperties key that is an invalid regex + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = + loadSchema("{\"type\":\"object\",\"patternProperties\":{\"***invalid\":{\"type\":\"string\"}}}"); + ASSERT_NE(nullptr, schema); + //Then compiling the schema should fail with INVALID_PATTERN like the + //string "pattern" keyword does, instead of silently storing the pattern + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_PATTERN, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +/* ── Runtime path building OOM: path_child_checked call sites (P3) ────── */ +/* All inject realloc inside celix_jansson_path_push, reached through + * path_child_checked (realloc <- path_push <- path_child_checked <- validator + * <- v_type <- root_validate, so level 4). */ + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidatePathChildCopyOomFail) { + //Given a nested object schema and realloc is injected to fail for the + //parent-token copy inside path_child_checked. The nested property adds two + //extra frames (v_object <- v_type), so the copy push is reached at level 6 + //(realloc <- path_push <- path_child_checked <- v_object(b) <- v_type(b) + //<- v_object(root) <- v_type(root) <- root_validate) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema( + "{\"type\":\"object\",\"properties\":{\"b\":{\"type\":\"object\",\"properties\":{\"a\":{\"type\":\"string\"}}}}}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + json_auto_t* instance = loadSchema("{\"b\":{\"a\":\"x\"}}"); + int errorCount = 0; + celix_ei_expect_realloc((void*)celix_jansson_schema_root_validate, 6, nullptr); + //Then validating fails closed with an out-of-memory error + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateRequiredPathOomFail) { + //Given a dependencies-array schema and realloc is injected to fail for the + //required-error path build in v_required. The v_required node is dispatched + //directly from obj_validate_deps, so the push is reached at level 6 + //(realloc <- path_push <- path_child_checked <- v_required <- + //obj_validate_deps <- v_object <- v_type <- root_validate) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"object\",\"dependencies\":{\"a\":[\"b\"]}}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + json_auto_t* instance = loadSchema("{\"a\":1}"); + int errorCount = 0; + celix_ei_expect_realloc((void*)celix_jansson_schema_root_validate, 6, nullptr); + //Then validating fails closed with an out-of-memory error + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidatePropertyNamesPathOomFail) { + //Given a propertyNames schema and realloc is injected to fail for the + //property-name path build + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"object\",\"propertyNames\":{\"type\":\"string\"}}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + json_auto_t* instance = loadSchema("{\"a\":1}"); + int errorCount = 0; + celix_ei_expect_realloc((void*)celix_jansson_schema_root_validate, 4, nullptr); + //Then validating fails closed with an out-of-memory error + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateDefaultPathOomFail) { + //Given a property with a default and realloc is injected to fail for the + //default-fill path build + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"object\",\"properties\":{\"a\":{\"type\":\"string\",\"default\":\"x\"}}}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + json_auto_t* instance = loadSchema("{}"); + int errorCount = 0; + celix_ei_expect_realloc((void*)celix_jansson_schema_root_validate, 4, nullptr); + //Then validating fails closed with an out-of-memory error + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateDepsPathOomFail) { + //Given a dependencies schema and realloc is injected to fail for the + //dependency path build + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"object\",\"dependencies\":{\"a\":{\"type\":\"string\"}}}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + json_auto_t* instance = loadSchema("{\"a\":\"x\"}"); + int errorCount = 0; + //obj_validate_deps adds a frame: realloc <- path_push <- path_child_checked + //<- obj_validate_deps <- v_object <- v_type <- root_validate, so level 5 + celix_ei_expect_realloc((void*)celix_jansson_schema_root_validate, 5, nullptr); + //Then validating fails closed with an out-of-memory error + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateArrayItemsPathOomFail) { + //Given an items schema and realloc is injected to fail for the item path build + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"array\",\"items\":{\"type\":\"string\"}}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + json_auto_t* instance = json_pack("[s]", "x"); + int errorCount = 0; + celix_ei_expect_realloc((void*)celix_jansson_schema_root_validate, 4, nullptr); + //Then validating fails closed with an out-of-memory error + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateTupleItemsPathOomFail) { + //Given a tuple items schema and realloc is injected to fail for the item path build + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"array\",\"items\":[{\"type\":\"string\"}]}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + json_auto_t* instance = json_pack("[s]", "x"); + int errorCount = 0; + celix_ei_expect_realloc((void*)celix_jansson_schema_root_validate, 4, nullptr); + //Then validating fails closed with an out-of-memory error + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateAdditionalItemsPathOomFail) { + //Given a tuple+additionalItems schema and realloc is injected to fail for + //the additional-item path build (2nd realloc: 1st = the tuple item push) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = + loadSchema("{\"type\":\"array\",\"items\":[{\"type\":\"string\"}],\"additionalItems\":{\"type\":\"string\"}}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + json_auto_t* instance = json_pack("[s,s]", "x", "y"); + int errorCount = 0; + celix_ei_expect_realloc((void*)celix_jansson_schema_root_validate, 4, nullptr, 2); + //Then validating fails closed with an out-of-memory error + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateContainsPathOomFail) { + //Given a contains schema and realloc is injected to fail for the contains + //element path build (after the first-sink allocation succeeds) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"array\",\"contains\":{\"type\":\"string\"}}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + json_auto_t* instance = json_pack("[s]", "x"); + int errorCount = 0; + celix_ei_expect_realloc((void*)celix_jansson_schema_root_validate, 4, nullptr); + //Then validating fails closed with an out-of-memory error + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaPatternPropertiesInvalidRegcompSecond) { + //Given patternProperties where the second pattern fails to compile, the + //first (compiled) entry must be released (regfree + unref) before the + //INVALID_PATTERN error propagates + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = + loadSchema("{\"type\":\"object\",\"patternProperties\":{\"a\":{\"type\":\"string\"},\"***invalid\":{\"type\":\"string\"}}}"); + ASSERT_NE(nullptr, schema); + //Then compiling the schema should fail with INVALID_PATTERN + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_PATTERN, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaDocFragmentDeriveOomFail) { + //Given strdup is injected to fail inside uri_update, hit by the + //document-fragment walk's $id base update for properties.x. Only the + //walk's derive has a scheme ("http://y/z" → path = strdup("/z")); the + //$ref URI derive ("#/properties/x") and the cur_base init ("") do not + //strdup, so ordinal 1 is the walk's derive. The failed walk returns NULL + //and the ref is resolved on the next resolve_external_refs iteration. + //properties.x is used (not definitions) so the walk is actually required. + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = + loadSchema("{\"properties\":{\"x\":{\"$id\":\"http://y/z\",\"type\":\"string\"}},\"$ref\":\"#/properties/x\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_strdup((void*)celix_jansson_uri_update, 0, nullptr); + //Then the schema still compiles (the failed walk self-heals on retry) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidatePathStrFallbackOomFail) { + //Given strdup is injected to fail for path_str's empty-string fallback + //(the only strdup inside path_str), making path_str return NULL — the + //only way emit_error_v's defensive empty-path fallback runs + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"string\"}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + json_auto_t* instance = json_integer(42); + int errorCount = 0; + celix_ei_expect_strdup((void*)celix_jansson_path_str, 0, nullptr); + //Then the type error is still reported with an empty path instead of NULL + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +/* ── Round 2: logic vec_push, invalid schema entries, default handling ── */ + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaLogicNotVecPushOomFail) { + //Given realloc is injected to fail in celix_jansson_vec_push, hit by the + //not-node push into the logic vec (the first vec_push of the compile) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"not\":{\"type\":\"string\"}}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_realloc((void*)celix_jansson_vec_push, 0, nullptr); + //Then compiling fails with NOMEM instead of leaking the not node and + //silently dropping the keyword + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaLogicComboVecPushOomFail) { + //Given realloc is injected to fail in celix_jansson_vec_push, hit by the + //combo-node push into the logic vec (the first vec_push of the compile) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"allOf\":[{\"type\":\"string\"}]}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_realloc((void*)celix_jansson_vec_push, 0, nullptr); + //Then compiling fails with NOMEM instead of leaking the combo node and + //silently dropping the keyword + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaRequiredNonStringElement) { + //Given a required array containing a non-string entry + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"object\",\"required\":[1]}"); + ASSERT_NE(nullptr, schema); + //Then compiling rejects the schema instead of crashing on strdup(NULL) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_SCHEMA, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaDependenciesNonStringElement) { + //Given a dependencies-array containing a non-string entry + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"object\",\"dependencies\":{\"a\":[1]}}"); + ASSERT_NE(nullptr, schema); + //Then compiling rejects the schema instead of crashing on strdup(NULL) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_SCHEMA, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateDefaultPatchPathBuildOomFail) { + //Given realloc is injected to fail for the second default-fill path + //build (the JSON Patch path; ordinal 2 — the first matching realloc is + //the default-value callback's path build) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = + loadSchema("{\"type\":\"object\",\"properties\":{\"a\":{\"type\":\"string\",\"default\":\"x\"}}}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + json_auto_t* instance = loadSchema("{}"); + int errorCount = 0; + celix_ei_expect_realloc((void*)celix_jansson_schema_root_validate, 4, nullptr, 2); + //Then validating fails closed with an out-of-memory error + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateDefaultPatchPathOomFail) { + //Given realloc is injected to fail in strbuf_append while building the + //default patch path (realloc <- strbuf_append <- appendc <- path_str, so + //level 0: strbuf_append is the realloc's direct caller) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = + loadSchema("{\"type\":\"object\",\"properties\":{\"a\":{\"type\":\"string\",\"default\":\"x\"}}}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + json_auto_t* instance = loadSchema("{}"); + int errorCount = 0; + celix_ei_expect_realloc((void*)celix_jansson_strbuf_append, 0, nullptr); + //Then the default patch is not applied and the validation fails closed + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaEmitErrorVasprintfFail) { + //Given vasprintf is injected to fail while formatting a validation + //error message (vasprintf <- emit_error_v <- emit_error <- v_type <- + //root_validate, so level 3) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"string\"}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + json_auto_t* instance = json_integer(42); + std::string message; + celix_ei_expect_vasprintf((void*)celix_jansson_schema_root_validate, 3, -1); + //Then the error is still reported with the OOM hint constant + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, captureMessageCb, &message, nullptr)); + EXPECT_EQ("out of memory: error message unavailable", message); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaDefaultDeepCopyOomFail) { + //Given json_deep_copy is injected to fail for the type-schema default + //(1st level-2 deep_copy: set_root_schema's two copies are level 1) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$id\":\"http://x/root\",\"type\":\"string\",\"default\":\"x\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_json_deep_copy((void*)celix_jansson_schema_set_root_schema, 2, nullptr); + //Then compiling fails with NOMEM instead of silently dropping the default + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaRefDefaultDeepCopyOomFail) { + //Given json_deep_copy is injected to fail for the $ref default + //(1st level-1 deep_copy: deep_copy <- depth <- set_root_schema) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"$ref\":\"#\",\"default\":\"x\"}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_json_deep_copy((void*)celix_jansson_schema_set_root_schema, 1, nullptr); + //Then compiling fails with NOMEM instead of silently dropping the default + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_NOMEM, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateDefaultPatchOomFail) { + //Given a property default and json_array_append_new is injected to fail + //inside celix_json_patch_add (its only append point) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = + loadSchema("{\"type\":\"object\",\"properties\":{\"a\":{\"type\":\"string\",\"default\":\"x\"}}}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + json_auto_t* instance = loadSchema("{}"); + int errorCount = 0; + celix_ei_expect_json_array_append_new((void*)celix_json_patch_add, 0, -1); + //Then validating fails closed with an out-of-memory error instead of + //silently dropping the default patch + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateRootDefaultPatchOomFail) { + //Given a root default (no type, so a null instance passes the type check) + //and json_array_append_new is injected to fail inside celix_json_patch_add + //(its only append point) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"default\":\"x\"}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + json_auto_t* instance = json_null(); + int errorCount = 0; + celix_ei_expect_json_array_append_new((void*)celix_json_patch_add, 0, -1); + //Then validating fails closed with an out-of-memory error instead of + //silently dropping the root default patch + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +/* ── strbuf append OOM: path_str, coll_propagate, fragment walk ───────── */ + +/* Message-capturing error callback: the coll_propagate OOM test needs the + * message CONTENT (prefixed vs raw) to tell the fallback from the normal + * path, which an error count alone cannot distinguish. */ +static std::vector g_collMsgs; +static void captureErrorCb(const char*, json_t*, const char* msg, void* ud) { + g_collMsgs.emplace_back(msg ? msg : ""); + int* count = static_cast(ud); + if (count) + (*count)++; +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidatePathStrOomFail) { + //Given realloc is injected to fail in strbuf_append, hit by the first + //append of path_str's path build (the '/' separator, ordinal 1 — no + //strbuf realloc precedes it in this flow) while reporting the type error + //at the nested "/a" path + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"type\":\"object\",\"properties\":{\"a\":{\"type\":\"string\"}}}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + json_auto_t* instance = loadSchema("{\"a\":42}"); + int errorCount = 0; + celix_ei_expect_realloc((void*)celix_jansson_strbuf_append, 0, nullptr, 1); + //Then the type error is still reported with the empty-path fallback + //instead of a silently truncated path, no crash + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); + + //And with a token long enough to realloc mid-build (the 64th char, ordinal + //2 — the first append grew the cap to 64), the failure is hit by a + //token-char append instead of the '/' separator (inner oom branch) + std::string key(64, 'a'); + std::string longSchema = + "{\"type\":\"object\",\"properties\":{\"" + key + "\":{\"type\":\"string\"}}}"; + std::string longInstance = "{\"" + key + "\":42}"; + celix_autoptr(celix_jansson_schema_validator_t) v2 = makeValidator(); + ASSERT_NE(nullptr, v2); + json_auto_t* schema2 = loadSchema(longSchema.c_str()); + ASSERT_NE(nullptr, schema2); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v2, schema2, nullptr)); + json_auto_t* instance2 = loadSchema(longInstance.c_str()); + errorCount = 0; + celix_ei_expect_realloc((void*)celix_jansson_strbuf_append, 0, nullptr, 2); + //Then the same empty-path fallback applies, no crash + EXPECT_EQ(1, celix_jansson_schema_validate(v2, instance2, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateRootDefaultPathStrOomFail) { + //Given realloc is injected to fail in strbuf_append, hit by path_str + //while applying a property-level default to a null instance (the "/a" + //path build, ordinal 1) — a root default would have an empty path and + //never append, so the property default is required here + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"properties\":{\"a\":{\"default\":5}}}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + json_auto_t* instance = loadSchema("{\"a\":null}"); + int errorCount = 0; + celix_ei_expect_realloc((void*)celix_jansson_strbuf_append, 0, nullptr, 1); + //Then validating fails closed with an out-of-memory error instead of + //crashing on a NULL patch path (json_string(NULL)), no leak of the value + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, countingErrorCb, &errorCount, nullptr)); + EXPECT_EQ(1, errorCount); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaValidateCollPropagateOomFail) { + //Given realloc is injected to fail in strbuf_append, hit by the first + //append of coll_propagate's prefixed-message build (ordinal 1 — the + //failing branch emits at the root path, whose empty path_str never + //appends, so no strbuf realloc precedes it) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + json_auto_t* schema = loadSchema("{\"anyOf\":[{\"type\":\"string\"}]}"); + ASSERT_NE(nullptr, schema); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); + json_auto_t* instance = json_integer(42); + int errorCount = 0; + g_collMsgs.clear(); + celix_ei_expect_realloc((void*)celix_jansson_strbuf_append, 0, nullptr, 1); + //Then both the anyOf failure and the branch error are still reported, the + //latter with the raw message instead of a truncated one (only the + //"[combination:" prefix is lost — two emissions either way, the message + //content is what distinguishes the fallback) + EXPECT_EQ(1, celix_jansson_schema_validate(v, instance, captureErrorCb, &errorCount, nullptr)); + EXPECT_EQ(2, errorCount); + ASSERT_EQ(2u, g_collMsgs.size()); + EXPECT_NE(std::string::npos, g_collMsgs[0].find("no subschema has succeeded")); + EXPECT_NE(std::string::npos, g_collMsgs[1].find("unexpected instance type")); + EXPECT_EQ(std::string::npos, g_collMsgs[1].find("[combination:")); +} + +TEST_F(JanssonExtSchemaErrorInjectionTestSuite, SchemaDocFragmentTokenDecodeOomFail) { + //Given realloc is injected to fail in strbuf_append, hit by the first + //append of the document-fragment walk's token decode (ordinal 2: 1 = + //percent_decode of the fragment inside uri_update, 2 = the walk's first + //token "definitions" — uri_location/uri_fragment allocate via strdup and + //malloc, not strbuf) + celix_autoptr(celix_jansson_schema_validator_t) v = makeValidator(); + ASSERT_NE(nullptr, v); + //"a" compiles before "b" registers, so its $ref forces the fragment walk + //(which aborts on OOM); the placeholder then resolves from the registry + //once "b" is registered — self-healing to OK + json_auto_t* schema = + loadSchema("{\"definitions\":{\"a\":{\"$ref\":\"#/definitions/b\"},\"b\":{\"type\":\"string\"}}}"); + ASSERT_NE(nullptr, schema); + celix_ei_expect_realloc((void*)celix_jansson_strbuf_append, 0, nullptr, 2); + //Then the walk aborts (a truncated token is never used for lookups) and + //the ref resolves from the registry on the next pass + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, nullptr)); +} diff --git a/libs/jansson_ext/gtest/src/test_abort.cpp b/libs/jansson_ext/gtest/src/test_abort.cpp new file mode 100644 index 000000000..cb4cdf15f --- /dev/null +++ b/libs/jansson_ext/gtest/src/test_abort.cpp @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "test_common.h" + +/* Helper: create a validator with abort enabled, load schema, validate, return error count */ +static int abort_validate(const char* schema_json, const char* instance_json) { + auto* v = celix_jansson_schema_validator_create( + nullptr, nullptr, + celix_jansson_schema_default_format_check, nullptr, + nullptr, nullptr); + if (!v) return -1; + celix_jansson_schema_validator_set_abort_on_error(v, true); + + json_error_t jerr; + json_t* sch = json_loads(schema_json, 0, &jerr); + if (!sch) { free_validator(v); return -1; } + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + free(errmsg); + if (rc != CELIX_JANSSON_SCHEMA_OK) { json_decref(sch); free_validator(v); return -1; } + + json_t* inst = json_loads(instance_json, JSON_DECODE_ANY, &jerr); + if (!inst) { json_decref(sch); free_validator(v); return -1; } + + reset_errors(); + int n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr); + json_decref(inst); + json_decref(sch); + free_validator(v); + return n; +} + +/* ── Test #1: v_type type slot abort ──────────────────────────────────── */ + +TEST(AbortTest, TypeSlotAbort) { + /* Type mismatch → abort before checking minLength */ + int n = abort_validate( + R"({"type":"string","minLength":3})", + "42"); + EXPECT_EQ(1, n); + EXPECT_EQ(1u, captured_errors.size()); + EXPECT_NE(std::string::npos, captured_messages[0].find("unexpected instance type")); +} + +/* ── Test #2: v_type logic combinator abort ───────────────────────────── */ + +TEST(AbortTest, LogicCombinatorAbort) { + /* allOf: first branch (type mismatch) fails → abort before second branch */ + int n = abort_validate( + R"({"allOf":[{"type":"string"},{"minLength":10}]})", + "42"); + EXPECT_EQ(1, n); + /* The combination itself reports 1 error; abort prevents further branches */ +} + +/* ── Test #3: v_type then branch abort ─────────────────────────────────── */ + +TEST(AbortTest, ThenBranchAbort) { + /* if passes (object), then requires name+age → only name failure captured */ + int n = abort_validate( + R"({"if":{"type":"object"},"then":{"required":["name","age"]}})", + R"({})"); + EXPECT_EQ(1, n); +} + +/* ── Test #4: v_type else branch abort ─────────────────────────────────── */ + +TEST(AbortTest, ElseBranchAbort) { + /* if fails (not string), else requires a+b → only "a" missing captured */ + int n = abort_validate( + R"({"if":{"type":"string"},"else":{"required":["a","b"]}})", + R"({})"); + EXPECT_EQ(1, n); +} + +/* ── Test #5: v_object required loop abort ────────────────────────────── */ + +TEST(AbortTest, RequiredLoopAbort) { + /* Missing a,b,c → only "a" reported */ + int n = abort_validate( + R"({"required":["a","b","c"]})", + R"({})"); + EXPECT_EQ(1, n); + EXPECT_NE(std::string::npos, captured_messages[0].find("'a'")); +} + +/* ── Test #6: v_object properties loop abort ──────────────────────────── */ + +TEST(AbortTest, PropertiesLoopAbort) { + /* x has wrong type → abort; y and z never checked */ + int n = abort_validate( + R"({"properties":{"x":{"type":"number"},"y":{"type":"string"},"z":{"type":"boolean"}}})", + R"({"x":"wrong","y":42,"z":"wrong"})"); + EXPECT_EQ(1, n); + EXPECT_NE(std::string::npos, captured_messages[0].find("unexpected instance type")); +} + +/* ── Test #7: v_object propertyNames abort ────────────────────────────── */ + +TEST(AbortTest, PropertyNamesAbort) { + /* "a" is too short (minLength=5) → abort; "bb" never checked */ + int n = abort_validate( + R"({"propertyNames":{"minLength":5}})", + R"({"a":1,"bb":2})"); + EXPECT_EQ(1, n); +} + +/* ── Test #8: v_object patternProperties abort ────────────────────────── */ + +TEST(AbortTest, PatternPropertiesAbort) { + /* ^x matches "x1" (wrong type) → abort; ^y pattern never checked */ + int n = abort_validate( + R"({"patternProperties":{"^x":{"type":"number"},"^y":{"type":"string"}}})", + R"({"x1":"wrong","y1":42})"); + EXPECT_EQ(1, n); +} + +/* ── Test #9: v_object additionalProperties abort ─────────────────────── */ + +TEST(AbortTest, AdditionalPropertiesAbort) { + /* "a" is not a number → abort; "b" never checked */ + int n = abort_validate( + R"({"additionalProperties":{"type":"number"}})", + R"({"a":"wrong","b":"wrong"})"); + EXPECT_EQ(1, n); + EXPECT_NE(std::string::npos, captured_messages[0].find("'a'")); +} + +/* ── Test #10: v_object dependencies loop abort ───────────────────────── */ + +TEST(AbortTest, DependenciesLoopAbort) { + /* a present → x missing → abort; b/y never checked */ + int n = abort_validate( + R"({"dependencies":{"a":["x"],"b":["y"]}})", + R"({"a":1,"b":2})"); + EXPECT_EQ(1, n); +} + +/* ── Test #11: v_array items_schema (uniform) abort ───────────────────── */ + +TEST(AbortTest, ItemsUniformAbort) { + /* First element "wrong" is not number → abort */ + int n = abort_validate( + R"({"items":{"type":"number"}})", + R"(["wrong",42,"also wrong"])"); + EXPECT_EQ(1, n); +} + +/* ── Test #12: v_array tuple items abort ──────────────────────────────── */ + +TEST(AbortTest, TupleItemsAbort) { + /* First position fails type check → abort */ + int n = abort_validate( + R"({"items":[{"type":"number"},{"type":"string"}]})", + R"(["wrong","ok"])"); + EXPECT_EQ(1, n); +} + +/* ── Test #13: v_array additionalItems abort ──────────────────────────── */ + +TEST(AbortTest, AdditionalItemsAbort) { + /* First extra element 100 is not string → abort */ + int n = abort_validate( + R"({"items":[{"type":"number"}],"additionalItems":{"type":"string"}})", + "[42,100,200]"); + EXPECT_EQ(1, n); +} + +/* ── Test #14: nested object abort propagation ────────────────────────── */ + +TEST(AbortTest, NestedObjectPropagation) { + /* Inner object missing "a" → abort propagates up */ + int n = abort_validate( + R"({"properties":{"inner":{"type":"object","properties":{"a":{"type":"number","minimum":1}},"required":["a"]}}})", + R"({"inner":{}})"); + EXPECT_EQ(1, n); +} + +/* ── Test #15: internal probe (not) does NOT trigger abort ────────────── */ + +TEST(AbortTest, InternalProbeNotDoesNotAbort) { + /* not with first_sink_t should not trigger abort; the real abort comes + * from x property failing type check */ + int n = abort_validate( + R"({"not":{"type":"string"},"properties":{"x":{"type":"number"}}})", + R"({"x":"wrong"})"); + EXPECT_EQ(1, n); + EXPECT_NE(std::string::npos, captured_messages[0].find("unexpected instance type")); +} + +/* ── Test #16: default behavior (abort disabled) ──────────────────────── */ + +TEST(AbortTest, DefaultBehaviorCollectsAll) { + /* Without abort, both property errors are reported */ + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + /* Explicitly NOT calling set_abort_on_error — default is false */ + + json_t* sch = json_loads(R"({"properties":{"a":{"type":"number"},"b":{"type":"number"}}})", 0, nullptr); + ASSERT_NE(nullptr, sch); + char* errmsg = nullptr; + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, sch, &errmsg)); + free(errmsg); + + json_t* inst = json_loads(R"({"a":"x","b":"y"})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + int n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr); + /* Both errors should be reported */ + EXPECT_GT(n, 1); + json_decref(inst); + json_decref(sch); + free_validator(v); +} + +/* ── Test #17: validate_uri also supports abort ───────────────────────── */ + +TEST(AbortTest, ValidateUriAbort) { + auto* v = celix_jansson_schema_validator_create( + nullptr, nullptr, + celix_jansson_schema_default_format_check, nullptr, + nullptr, nullptr); + ASSERT_NE(nullptr, v); + celix_jansson_schema_validator_set_abort_on_error(v, true); + + json_t* sch = json_loads( + R"({"definitions":{"T":{"type":"integer","minimum":10}}})", 0, nullptr); + ASSERT_NE(nullptr, sch); + char* errmsg = nullptr; + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, sch, &errmsg)); + free(errmsg); + + json_t* inst = json_loads(R"("wrong")", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + int n = celix_jansson_schema_validate_uri(v, inst, "#/definitions/T", capture_error, nullptr, nullptr); + EXPECT_EQ(1, n); + json_decref(inst); + json_decref(sch); + free_validator(v); +} diff --git a/libs/jansson_ext/gtest/src/test_combinations.cpp b/libs/jansson_ext/gtest/src/test_combinations.cpp new file mode 100644 index 000000000..a7a8b2cf0 --- /dev/null +++ b/libs/jansson_ext/gtest/src/test_combinations.cpp @@ -0,0 +1,323 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "test_common.h" + +/* ── allOf ─────────────────────────────────────────────────────────────── */ + +TEST(CombinationsTest, AllOfBothPass) { + static const char* schema = R"({ + "allOf": [ + {"type": "integer", "minimum": 1}, + {"type": "integer", "maximum": 10} + ] + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Valid: 5 passes both subschemas */ + reset_errors(); + json_t* inst = json_loads("5", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + EXPECT_EQ(0, celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Invalid: 0 fails minimum */ + inst = json_loads("0", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr), 0); + json_decref(inst); + + /* Invalid: 11 fails maximum */ + inst = json_loads("11", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr), 0); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── anyOf ─────────────────────────────────────────────────────────────── */ + +TEST(CombinationsTest, AnyOfOnePass) { + static const char* schema = R"({ + "anyOf": [ + {"type": "string"}, + {"type": "integer"} + ] + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, sch, &errmsg)); + free(errmsg); + + /* Valid: string matches first subschema */ + reset_errors(); + json_t* inst = json_loads("\"hello\"", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + EXPECT_EQ(0, celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Valid: integer matches second subschema */ + inst = json_loads("42", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Invalid: boolean matches neither */ + inst = json_loads("true", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr), 0); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── oneOf ─────────────────────────────────────────────────────────────── */ + +TEST(CombinationsTest, OneOfExactlyOne) { + static const char* schema = R"({ + "oneOf": [ + {"type": "integer", "minimum": 10}, + {"type": "integer", "multipleOf": 2} + ] + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, sch, &errmsg)); + free(errmsg); + + /* Valid: 7 is not >=10 and not multiple of 2 → but wait, oneOf requires exactly ONE match, + * and 7 matches NEITHER. So it's invalid. */ + /* Let's use better examples: */ + + /* 12: >=10 (yes) and multiple of 2 (yes) → both pass → invalid (multiple match) */ + reset_errors(); + json_t* inst = json_loads("12", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + EXPECT_GT(celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr), 0); + json_decref(inst); + + /* 11: >=10 (yes), not multiple of 2 → exactly one → valid */ + inst = json_loads("11", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr)); + json_decref(inst); + + /* 4: <10 (no), but multiple of 2 (yes) → exactly one → valid */ + inst = json_loads("4", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr)); + json_decref(inst); + + /* 7: <10 (no), not multiple of 2 (no) → zero matches → invalid */ + inst = json_loads("7", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr), 0); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── not ───────────────────────────────────────────────────────────────── */ + +TEST(CombinationsTest, NotSchema) { + static const char* schema = R"({ + "not": { "type": "string" } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, sch, &errmsg)); + free(errmsg); + + /* Valid: 42 is not a string */ + reset_errors(); + json_t* inst = json_loads("42", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + EXPECT_EQ(0, celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Invalid: "hello" is a string — not requires it to NOT validate */ + inst = json_loads("\"hello\"", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr), 0); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── Verbose combination errors ────────────────────────────────────────── */ + +TEST(CombinationsTest, VerboseErrors) { + /* Test that combination errors include case# prefixes */ + static const char* schema = R"({ + "allOf": [ + {"type": "integer", "minimum": 5}, + {"type": "integer", "multipleOf": 2} + ] + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, sch, &errmsg)); + free(errmsg); + + /* 3 fails both minimum and multipleOf → should get verbose errors */ + reset_errors(); + json_t* inst = json_loads("3", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + int n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr); + EXPECT_GT(n, 0); + EXPECT_GE(captured_messages.size(), 1u) << "Should have at least 1 error message"; + json_decref(inst); + json_decref(sch); + free_validator(v); +} + +/* ── Nested combinations ───────────────────────────────────────────────── */ + +TEST(CombinationsTest, NestedCombinations) { + static const char* schema = R"({ + "allOf": [ + {"anyOf": [{"type": "integer"}, {"type": "boolean"}]}, + {"not": {"type": "boolean"}} + ] + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, sch, &errmsg)); + free(errmsg); + + /* 42: integer (passes anyOf[0]), not boolean (passes not) → valid */ + reset_errors(); + json_t* inst = json_loads("42", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + EXPECT_EQ(0, celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr)); + json_decref(inst); + + /* "hello": string fails anyOf (neither integer nor boolean), fails allOf → invalid */ + inst = json_loads("\"hello\"", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr), 0); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── if/then/else ──────────────────────────────────────────────────────── */ + +TEST(CombinationsTest, IfThenElse) { + static const char* schema = R"({ + "if": { "type": "string" }, + "then": { "minLength": 3 }, + "else": { "type": "integer", "minimum": 0 } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, sch, &errmsg)); + free(errmsg); + + /* "ab": string, activates 'then', minLength=3 fails → invalid */ + reset_errors(); + json_t* inst = json_loads("\"ab\"", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + EXPECT_GT(celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr), 0); + json_decref(inst); + + /* "abc": string, activates 'then', minLength=3 passes → valid */ + inst = json_loads("\"abc\"", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr)); + json_decref(inst); + + /* 42: not string, activates 'else', integer >= 0 → valid */ + inst = json_loads("42", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr)); + json_decref(inst); + + /* -1: not string, activates 'else', integer < 0 → invalid */ + inst = json_loads("-1", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr), 0); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} diff --git a/libs/jansson_ext/gtest/src/test_common.h b/libs/jansson_ext/gtest/src/test_common.h new file mode 100644 index 000000000..d3c679adc --- /dev/null +++ b/libs/jansson_ext/gtest/src/test_common.h @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#ifndef CELIX_JANSSON_GTEST_TEST_COMMON_H +#define CELIX_JANSSON_GTEST_TEST_COMMON_H + +#include "celix_jansson_schema.h" +#include +#include +#include + +/** Captures validation error pointer strings for assertion. */ +static std::vector captured_errors; +static std::vector captured_messages; + +static void capture_error(const char* ptr, json_t* /*instance*/, const char* msg, void* /*ud*/) { + captured_errors.push_back(ptr ? ptr : ""); + captured_messages.push_back(msg ? msg : ""); +} + +static void reset_errors() { + captured_errors.clear(); + captured_messages.clear(); +} + +/** + * Test fixture providing common setup for validator tests. + */ +class ValidatorTest : public ::testing::Test { + protected: + celix_jansson_schema_validator_t* v_ = nullptr; + json_t* schema_ = nullptr; + int last_err_ = CELIX_JANSSON_SCHEMA_OK; + + void SetUp() override { + v_ = celix_jansson_schema_validator_create(nullptr, + nullptr, /* loader */ + celix_jansson_schema_default_format_check, + nullptr, /* format */ + nullptr, + nullptr /* content */ + ); + ASSERT_NE(nullptr, v_); + reset_errors(); + } + + void TearDown() override { + celix_jansson_schema_validator_destroy(v_); + v_ = nullptr; + json_decref(schema_); + schema_ = nullptr; + reset_errors(); + } + + /** Load a JSON Schema string into the validator. */ + void load_schema(const char* schema_json) { + json_error_t jerr; + json_decref(schema_); + schema_ = json_loads(schema_json, 0, &jerr); + ASSERT_NE(nullptr, schema_) << "JSON parse error: " << jerr.text; + + char* errmsg = nullptr; + last_err_ = celix_jansson_schema_set_root_schema(v_, schema_, &errmsg); + if (last_err_ != CELIX_JANSSON_SCHEMA_OK && errmsg) { + free(errmsg); + } + } + + /** Validate an instance and return error count. */ + int run_validate(const char* instance_json, json_t** patch_out = nullptr) { + json_error_t jerr; + json_t* inst = json_loads(instance_json, JSON_DECODE_ANY, &jerr); + if (!inst) { + ADD_FAILURE() << "JSON parse error: " << jerr.text; + return -1; + } + + reset_errors(); + int n = celix_jansson_schema_validate(v_, inst, capture_error, nullptr, patch_out); + json_decref(inst); + return n; + } + + /** Assert validation succeeds. */ + void assert_valid(const char* instance_json) { + int n = run_validate(instance_json); + EXPECT_EQ(0, n) << "Expected valid, got " << n << " errors."; + } + + /** Assert validation fails with at least 1 error. */ + void assert_invalid(const char* instance_json, int expected_errors) { + int n = run_validate(instance_json); + EXPECT_EQ(expected_errors, n) << "Expected " << expected_errors << " errors, got " << n; + } +}; + +/* Standalone helpers for non-fixture tests */ +[[maybe_unused]] static celix_jansson_schema_validator_t* make_validator() { + return celix_jansson_schema_validator_create( + nullptr, nullptr, celix_jansson_schema_default_format_check, nullptr, nullptr, nullptr); +} + +[[maybe_unused]] static void free_validator(celix_jansson_schema_validator_t* v) { celix_jansson_schema_validator_destroy(v); } + +#endif /* CELIX_JANSSON_GTEST_TEST_COMMON_H */ diff --git a/libs/jansson_ext/gtest/src/test_defaults.cpp b/libs/jansson_ext/gtest/src/test_defaults.cpp new file mode 100644 index 000000000..06736d8a1 --- /dev/null +++ b/libs/jansson_ext/gtest/src/test_defaults.cpp @@ -0,0 +1,631 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "celix_json_patch.h" +#include "test_common.h" + +/* ── Object property default ──────────────────────────────────────────── */ + +TEST(DefaultsTest, ObjectDefault) { + static const char* schema = R"({ + "type": "object", + "properties": { + "name": { "type": "string" }, + "age": { "type": "integer", "default": 18 } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Validate with missing "age" — a patch should be generated */ + reset_errors(); + json_t* patch = nullptr; + json_t* inst = json_loads(R"({"name":"Bob"})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + int n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, &patch); + EXPECT_EQ(0, n); + + /* Check patch */ + ASSERT_NE(nullptr, patch); + EXPECT_TRUE(json_is_array(patch)); + EXPECT_GE(json_array_size(patch), 1u); + + json_decref(inst); + json_decref(patch); + json_decref(sch); + free_validator(v); +} + +/* ── Root default for null instance ───────────────────────────────────── */ + +TEST(DefaultsTest, RootDefaultForNull) { + static const char* schema = R"({ + "type": "object", + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string", "default": "unknown" } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Validate with empty object — defaults should be generated */ + reset_errors(); + json_t* patch = nullptr; + json_t* inst = json_loads("{}", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + int n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, &patch); + EXPECT_EQ(0, n); + + ASSERT_NE(nullptr, patch); + EXPECT_TRUE(json_is_array(patch)); + + json_decref(inst); + json_decref(patch); + json_decref(sch); + free_validator(v); +} + +/* ── Boolean schema default ───────────────────────────────────────────── */ + +TEST(DefaultsTest, BooleanSchemaNoDefault) { + /* Boolean schemas don't have defaults */ + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_true(); + char* errmsg = nullptr; + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, sch, &errmsg)); + free(errmsg); + + json_t* patch = nullptr; + json_t* inst = json_loads("42", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + int n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, &patch); + EXPECT_EQ(0, n); + + /* No defaults in boolean schema → empty or null patch */ + if (patch) { + EXPECT_TRUE(json_is_array(patch)); + } + + json_decref(inst); + json_decref(patch); + json_decref(sch); + free_validator(v); +} + +/* ── Nested object defaults ───────────────────────────────────────────── */ + +TEST(DefaultsTest, NestedDefaults) { + static const char* schema = R"({ + "type": "object", + "properties": { + "address": { + "type": "object", + "properties": { + "street": { "type": "string", "default": "Main St" }, + "city": { "type": "string", "default": "Springfield" } + } + } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Validate with empty object — nested defaults should be generated */ + reset_errors(); + json_t* patch = nullptr; + json_t* inst = json_loads("{}", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + int n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, &patch); + EXPECT_EQ(0, n); + + /* Should get a patch with defaults */ + ASSERT_NE(nullptr, patch); + + json_decref(inst); + json_decref(patch); + json_decref(sch); + free_validator(v); +} + +/* ── Default value persistence (issue-25 pattern) ─────────────────────── */ + +TEST(DefaultsTest, DefaultPersistsOnValid) { + static const char* schema = R"({ + "type": "object", + "properties": { + "name": { "type": "string" }, + "address": { + "type": "object", + "properties": { + "street": { "type": "string", "default": "Abbey Road" } + } + } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Instance with name and empty address — street default should be generated */ + reset_errors(); + json_t* patch = nullptr; + json_t* inst = json_loads(R"({"name":"Hans","age":69,"address":{}})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + int n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, &patch); + EXPECT_EQ(0, n); + + ASSERT_NE(nullptr, patch); + EXPECT_TRUE(json_is_array(patch)); + + json_decref(inst); + json_decref(patch); + json_decref(sch); + free_validator(v); +} + +/* ── Patch application ────────────────────────────────────────────────── */ + +TEST(DefaultsTest, PatchApply) { + json_t* original = json_loads("{\"a\":1}", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, original); + + json_t* patch = json_array(); + json_t* add_op = json_object(); + json_object_set_new(add_op, "op", json_string("add")); + json_object_set_new(add_op, "path", json_string("/b")); + json_object_set_new(add_op, "value", json_integer(2)); + json_array_append_new(patch, add_op); + + json_t* result = celix_json_patch_apply(original, patch); + ASSERT_NE(nullptr, result); + json_t* expected = json_loads("{\"a\":1,\"b\":2}", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, expected); + EXPECT_TRUE(json_equal(result, expected)); + EXPECT_FALSE(json_equal(original, result)); + + json_decref(original); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +TEST(DefaultsTest, PatchApplyNested) { + json_t* original = json_loads("{}", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, original); + + json_t* patch = json_array(); + json_t* add_op = json_object(); + json_object_set_new(add_op, "op", json_string("add")); + json_object_set_new(add_op, "path", json_string("/address/street")); + json_object_set_new(add_op, "value", json_string("Main St")); + json_array_append_new(patch, add_op); + + json_t* result = celix_json_patch_apply(original, patch); + ASSERT_NE(nullptr, result); + json_t* expected = json_loads(R"({"address":{"street":"Main St"}})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, expected); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(original); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +TEST(DefaultsTest, EndToEndDefaultsWithApply) { + static const char* schema = R"({ + "type": "object", + "properties": { + "name": { "type": "string", "default": "anonymous" }, + "age": { "type": "integer", "default": 0 } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, sch, &errmsg)); + free(errmsg); + + json_t* inst = json_loads("{}", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + + reset_errors(); + json_t* patch = nullptr; + int n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, &patch); + EXPECT_EQ(0, n); + + json_t* filled = celix_json_patch_apply(inst, patch); + ASSERT_NE(nullptr, filled); + + json_t* expected = json_loads(R"({"name":"anonymous","age":0})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, expected); + EXPECT_TRUE(json_equal(filled, expected)); + EXPECT_FALSE(json_equal(inst, filled)); + + json_decref(inst); + json_decref(patch); + json_decref(filled); + json_decref(expected); + json_decref(sch); + free_validator(v); +} + +/* ── Default value with object type ───────────────────────────────────── */ + +TEST(DefaultsTest, ObjectTypeDefault) { + static const char* schema = R"({ + "type": "object", + "properties": { + "config": { + "type": "object", + "default": { + "timeout": 30, + "retries": 3, + "endpoint": { + "host": "localhost", + "port": 8080 + } + }, + "properties": { + "timeout": { "type": "integer" }, + "retries": { "type": "integer" }, + "endpoint": { + "type": "object", + "properties": { + "host": { "type": "string" }, + "port": { "type": "integer" } + } + } + } + } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, sch, &errmsg)); + free(errmsg); + + /* Validate empty object — "config" should get the full default object */ + json_t* inst = json_loads("{}", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + + reset_errors(); + json_t* patch = nullptr; + int n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, &patch); + EXPECT_EQ(0, n); + ASSERT_NE(nullptr, patch); + + /* Apply patch to get filled document */ + json_t* filled = celix_json_patch_apply(inst, patch); + ASSERT_NE(nullptr, filled); + + /* filled should have the nested config object */ + json_t* cfg = json_object_get(filled, "config"); + ASSERT_NE(nullptr, cfg); + EXPECT_TRUE(json_is_object(cfg)); + + json_t* timeout = json_object_get(cfg, "timeout"); + ASSERT_NE(nullptr, timeout); + EXPECT_EQ(30, json_integer_value(timeout)); + + json_t* retries = json_object_get(cfg, "retries"); + ASSERT_NE(nullptr, retries); + EXPECT_EQ(3, json_integer_value(retries)); + + json_t* endpoint = json_object_get(cfg, "endpoint"); + ASSERT_NE(nullptr, endpoint); + EXPECT_TRUE(json_is_object(endpoint)); + + json_t* host = json_object_get(endpoint, "host"); + ASSERT_NE(nullptr, host); + EXPECT_STREQ("localhost", json_string_value(host)); + + json_t* port = json_object_get(endpoint, "port"); + ASSERT_NE(nullptr, port); + EXPECT_EQ(8080, json_integer_value(port)); + + /* Original unchanged */ + EXPECT_FALSE(json_equal(inst, filled)); + + json_decref(inst); + json_decref(patch); + json_decref(filled); + json_decref(sch); + free_validator(v); +} + +/* ── Object default only partially overridden by instance ──────────────── */ +/* Per JSON Schema semantics, the object-level "default" applies when the + * entire property is MISSING. When the property is present (even partially), + * only per-property defaults are used. */ + +TEST(DefaultsTest, ObjectDefaultWhenMissing) { + static const char* schema = R"({ + "type": "object", + "properties": { + "config": { + "type": "object", + "default": { + "timeout": 30, + "retries": 3 + }, + "properties": { + "timeout": { "type": "integer" }, + "retries": { "type": "integer" } + } + } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, sch, &errmsg)); + free(errmsg); + + /* Case 1: config is MISSING — the full object default should be inserted */ + json_t* inst = json_loads(R"({})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + + reset_errors(); + json_t* patch = nullptr; + int n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, &patch); + EXPECT_EQ(0, n); + ASSERT_NE(nullptr, patch); + + json_t* filled = celix_json_patch_apply(inst, patch); + ASSERT_NE(nullptr, filled); + + json_t* cfg = json_object_get(filled, "config"); + ASSERT_NE(nullptr, cfg) << "config should be inserted from object default"; + EXPECT_EQ(30, json_integer_value(json_object_get(cfg, "timeout"))); + EXPECT_EQ(3, json_integer_value(json_object_get(cfg, "retries"))); + + json_decref(inst); + json_decref(patch); + json_decref(filled); + + /* Case 2: config is present but with per-property defaults */ + /* For per-property filling, each property needs its own "default" */ + json_decref(sch); + celix_jansson_schema_validator_destroy(v); +} + +TEST(DefaultsTest, PerPropertyDefaultPartialOverride) { + /* Each property has its own default — partial fill should work */ + static const char* schema = R"({ + "type": "object", + "properties": { + "config": { + "type": "object", + "properties": { + "timeout": { "type": "integer", "default": 30 }, + "retries": { "type": "integer", "default": 3 } + } + } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, sch, &errmsg)); + free(errmsg); + + /* Instance provides config.timeout but not config.retries */ + json_t* inst = json_loads(R"({"config":{"timeout":60}})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + + reset_errors(); + json_t* patch = nullptr; + int n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, &patch); + EXPECT_EQ(0, n); + ASSERT_NE(nullptr, patch); + + json_t* filled = celix_json_patch_apply(inst, patch); + ASSERT_NE(nullptr, filled); + + json_t* cfg = json_object_get(filled, "config"); + ASSERT_NE(nullptr, cfg); + + /* timeout should be 60 (from instance), retries should be 3 (from per-property default) */ + EXPECT_EQ(60, json_integer_value(json_object_get(cfg, "timeout"))); + + json_t* rv = json_object_get(cfg, "retries"); + ASSERT_NE(nullptr, rv) << "retries should be filled from per-property default"; + EXPECT_EQ(3, json_integer_value(rv)); + + json_decref(inst); + json_decref(patch); + json_decref(filled); + json_decref(sch); + free_validator(v); +} + +/* ── Array of objects with defaults ────────────────────────────────────── */ + +TEST(DefaultsTest, ArrayOfObjectsDefault) { + static const char* schema = R"({ + "type": "object", + "properties": { + "items": { + "type": "array", + "default": [], + "items": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string", "default": "untitled" } + } + } + } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, sch, &errmsg)); + free(errmsg); + + /* Instance with nested object missing name */ + json_t* inst = json_loads(R"({"items":[{"id":1}]})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + + reset_errors(); + json_t* patch = nullptr; + int n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, &patch); + EXPECT_EQ(0, n); + ASSERT_NE(nullptr, patch); + + json_t* filled = celix_json_patch_apply(inst, patch); + ASSERT_NE(nullptr, filled); + + json_t* arr = json_object_get(filled, "items"); + ASSERT_NE(nullptr, arr); + EXPECT_EQ(1, json_array_size(arr)); + + json_t* elem0 = json_array_get(arr, 0); + ASSERT_NE(nullptr, elem0); + EXPECT_EQ(1, json_integer_value(json_object_get(elem0, "id"))); + EXPECT_STREQ("untitled", json_string_value(json_object_get(elem0, "name"))); + + json_decref(inst); + json_decref(patch); + json_decref(filled); + json_decref(sch); + free_validator(v); +} + +/* ── RFC 6901 escaping in default patch paths ─────────────────────────── */ + +TEST(DefaultsTest, DefaultPatchPathEscaping) { + /* Keys containing '/' and '~' must be escaped (~1/~0) in the JSON Patch + * path per RFC 6901. The patch path is built via the path API, so the + * paths below are the escaped forms. */ + static const char* schema = R"({ + "type": "object", + "properties": { + "a/b": { "type": "string", "default": "x" }, + "a~b": { "type": "string", "default": "y" } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Validate with empty object — both defaults generate a patch op */ + reset_errors(); + json_t* patch = nullptr; + json_t* inst = json_loads("{}", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + int n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, &patch); + EXPECT_EQ(0, n); + + ASSERT_NE(nullptr, patch); + ASSERT_TRUE(json_is_array(patch)); + ASSERT_EQ(2u, json_array_size(patch)); + + bool saw_slash_escaped = false; + bool saw_tilde_escaped = false; + json_t* op; + size_t i; + json_array_foreach(patch, i, op) { + const char* path = json_string_value(json_object_get(op, "path")); + ASSERT_NE(nullptr, path); + if (std::string(path) == "/a~1b") + saw_slash_escaped = true; + if (std::string(path) == "/a~0b") + saw_tilde_escaped = true; + } + EXPECT_TRUE(saw_slash_escaped); + EXPECT_TRUE(saw_tilde_escaped); + + json_decref(inst); + json_decref(patch); + json_decref(sch); + free_validator(v); +} diff --git a/libs/jansson_ext/gtest/src/test_errors.cpp b/libs/jansson_ext/gtest/src/test_errors.cpp new file mode 100644 index 000000000..ed6ee8dfd --- /dev/null +++ b/libs/jansson_ext/gtest/src/test_errors.cpp @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "test_common.h" + +static const char* person_schema = R"({ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": { "type": "string" }, + "age": { "type": "integer", "minimum": 2, "maximum": 200 }, + "phones": { "type": "array", "items": { "type": "integer" } } + }, + "required": ["name", "age"], + "additionalProperties": false +})"; + +class ErrorsTest : public ValidatorTest {}; + +TEST_F(ErrorsTest, ValidPerson) { + load_schema(person_schema); + assert_valid(R"({"name": "John", "age": 42})"); +} + +TEST_F(ErrorsTest, MissingRequiredName) { + load_schema(person_schema); + reset_errors(); + int n = run_validate(R"({"age": 42})"); + EXPECT_EQ(1, n); + if (!captured_errors.empty()) { + EXPECT_EQ("", captured_errors[0]); /* root pointer for missing required */ + } +} + +TEST_F(ErrorsTest, WrongTypeForName) { + load_schema(person_schema); + reset_errors(); + int n = run_validate(R"({"name": 123, "age": 42})"); + EXPECT_GT(n, 0); +} + +TEST_F(ErrorsTest, AdditionalProperty) { + load_schema(person_schema); + reset_errors(); + int n = run_validate(R"({"name": "John", "age": 42, "street": "Main"})"); + EXPECT_GT(n, 0); +} + +TEST_F(ErrorsTest, ArrayItemTypeError) { + load_schema(person_schema); + reset_errors(); + int n = run_validate(R"({"name": "John", "age": 42, "phones": [123, "abc"]})"); + EXPECT_GT(n, 0); +} + +// Enum and const tests +static const char* enum_schema = R"({ + "type": "string", + "enum": ["red", "green", "blue"] +})"; + +TEST_F(ErrorsTest, EnumValid) { + load_schema(enum_schema); + assert_valid(R"("red")"); +} + +TEST_F(ErrorsTest, EnumInvalid) { + load_schema(enum_schema); + assert_invalid(R"("yellow")", 1); +} + +static const char* const_schema = R"({ + "type": "integer", + "const": 42 +})"; + +TEST_F(ErrorsTest, ConstValid) { + load_schema(const_schema); + assert_valid("42"); +} + +TEST_F(ErrorsTest, ConstInvalid) { + load_schema(const_schema); + assert_invalid("43", 1); +} diff --git a/libs/jansson_ext/gtest/src/test_format_check.cpp b/libs/jansson_ext/gtest/src/test_format_check.cpp new file mode 100644 index 000000000..d7a5495fd --- /dev/null +++ b/libs/jansson_ext/gtest/src/test_format_check.cpp @@ -0,0 +1,198 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "test_common.h" + +struct FormatCase { + const char* format; + const char* value; + bool valid; +}; + +TEST(FormatCheckTest, DateTime) { + static const FormatCase cases[] = { + {"date-time", "1985-04-12T23:20:50Z", true}, + {"date-time", "1985-04-12T23:20:50.123Z", true}, + {"date-time", "1985-04-12T23:20:50+01:00", true}, + {"date-time", "1985-04-12T23:20:50-05:00", true}, + {"date-time", "2016-12-31T23:59:60Z", true}, /* leap second */ + {"date-time", "2016-12-31T23:59:60.123Z", true}, /* leap second with fraction */ + {"date-time", "2020-02-29T12:00:00Z", true}, /* leap year */ + {"date-time", "2016-06-30T23:59:60Z", true}, /* leap second */ + /* "2016-06-30T23:59:60Z" is valid per RFC 3339 */ + {"date-time", "2019-02-29T12:00:00Z", false}, /* not a leap year */ + {"date-time", "1985-04-12T24:00:00Z", false}, /* hour 24 invalid */ + {"date-time", "1985-4-12T23:20:50Z", false}, /* no zero padding */ + {"date-time", "1985-04-12t23:20:50Z", true}, /* lowercase T */ + {"date-time", "1985-04-12T23:20:50+24:00", false}, /* offset too large */ + {"date-time", "not-a-date", false}, + {"date-time", "2020-01-01", false}, /* missing T separator */ + {"date-time", "2020-01-01T10:30:00", false}, /* missing timezone */ + {"date-time", "2020-01-01T+01:00", false}, /* empty time part */ + {"date-time", "2020-01-01T10:30:00.123456789012345678901234567890Z", false}, /* time part >= 32 chars */ + {"date-time", "2020-01-01T00:00:60Z", false}, /* leap second not at 23:59:60 UTC */ + {"date-time", "2020-01-01T10:30:00+12x30", false}, /* malformed timezone */ + {"date-time", "2020-01-01T10:30:00+1230", false}, /* timezone missing colon */ + }; + + for (auto& c : cases) { + int rc = celix_jansson_schema_default_format_check(c.format, c.value, nullptr); + if (c.valid) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << "Expected valid " << c.format << ": " << c.value; + else + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, rc) << "Expected invalid " << c.format << ": " << c.value; + } +} + +TEST(FormatCheckTest, Ipv4) { + static const FormatCase cases[] = { + {"ipv4", "192.168.1.1", true}, + {"ipv4", "0.0.0.0", true}, + {"ipv4", "255.255.255.255", true}, + {"ipv4", "127.0.0.1", true}, + {"ipv4", "256.0.0.0", false}, + {"ipv4", "1.2.3.4.5", false}, + {"ipv4", "192.168.1", false}, + {"ipv4", "abc.def.ghi.jkl", false}, + }; + + for (auto& c : cases) { + int rc = celix_jansson_schema_default_format_check(c.format, c.value, nullptr); + if (c.valid) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << "Expected valid ipv4: " << c.value; + else + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, rc) << "Expected invalid ipv4: " << c.value; + } +} + +TEST(FormatCheckTest, Uuid) { + static const FormatCase cases[] = { + {"uuid", "12345678-1234-1234-1234-123456789abc", true}, + {"uuid", "00000000-0000-0000-0000-000000000000", true}, + {"uuid", "abcdefab-abcd-abcd-abcd-abcdefabcdef", true}, + {"uuid", "12345678-1234-1234-1234-123456789ab", false}, /* too short */ + {"uuid", "12345678-1234-1234-1234-123456789abcde", false}, /* too long */ + {"uuid", "gggggggg-gggg-gggg-gggg-gggggggggggg", false}, + }; + + for (auto& c : cases) { + int rc = celix_jansson_schema_default_format_check(c.format, c.value, nullptr); + if (c.valid) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << "Expected valid uuid: " << c.value; + else + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, rc) << "Expected invalid uuid: " << c.value; + } +} + +TEST(FormatCheckTest, Regex) { + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_default_format_check("regex", "^a*$", nullptr)); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_default_format_check("regex", "[a-z]+", nullptr)); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_default_format_check("regex", ".*", nullptr)); + /* Unbalanced bracket should fail to compile */ + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_default_format_check("regex", "[", nullptr)); +} + +TEST(FormatCheckTest, TimeLeapSecond) { + /* Leap second only valid when UTC-normalized time is 23:59:60 */ + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("time", "23:59:60Z", nullptr)); + /* Normalizes to UTC 23:59:60 */ + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("time", "01:29:60+01:30", nullptr)); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("time", "15:59:60-08:00", nullptr)); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("time", "23:29:60+23:30", nullptr)); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("time", "00:29:60-23:30", nullptr)); + /* Invalid: wrong hour after normalization */ + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("time", "22:59:60Z", nullptr)); + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("time", "23:59:60+01:00", nullptr)); + /* Invalid: wrong minute after normalization */ + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("time", "23:58:60Z", nullptr)); + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("time", "23:59:60-00:30", nullptr)); +} + +TEST(FormatCheckTest, UriInvalidChars) { + /* Valid URIs */ + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("uri", "http://example.com", nullptr)); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("uri", "http://example.com/path/segment", nullptr)); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("uri", "http://example.com/path%20encoded", nullptr)); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("uri", "http://example.com?q=search&k=v", nullptr)); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("uri", "http://example.com#fragment", nullptr)); + /* Percent-encoded query/fragment chars */ + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("uri", "http://a.b?q=%3F%2F%23", nullptr)); + /* Invalid: space in path */ + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("uri", "http://example.com/path with spaces", nullptr)); + /* Invalid: bad percent encoding */ + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("uri", "http://example.com/path%ZZ", nullptr)); + /* Invalid: bare double quote in path (not pct-encoded) */ + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("uri", "http://example.com/path\"quote", nullptr)); +} + +TEST(FormatCheckTest, UriAuthority) { + /* Valid URIs */ + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("uri", "http://[::1]:8080/", nullptr)); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("uri", "http://a%20b/", nullptr)); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("uri", "http://example.com:8080/", nullptr)); + /* Invalid: empty URI */ + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("uri", "", nullptr)); + /* Invalid: unclosed IPv6 literal */ + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("uri", "http://[::1", nullptr)); + /* Invalid: bad percent-encoding in reg-name */ + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("uri", "http://a%zz/", nullptr)); + /* Invalid: non-digit in port */ + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("uri", "http://example.com:8a80/", nullptr)); + /* Invalid: space in query */ + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("uri", "http://a.b/?x y", nullptr)); + /* Invalid: space in fragment */ + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("uri", "http://a.b/#x y", nullptr)); +} + +/* ── NULL argument guard ─────────────────────────────────────────────────── */ + +TEST(FormatCheckTest, NullArgumentDispatch) { + /* NULL format */ + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT, + celix_jansson_schema_default_format_check(nullptr, "valid-value", nullptr)); + /* NULL value */ + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT, + celix_jansson_schema_default_format_check("date-time", nullptr, nullptr)); +} diff --git a/libs/jansson_ext/gtest/src/test_format_check_extended.cpp b/libs/jansson_ext/gtest/src/test_format_check_extended.cpp new file mode 100644 index 000000000..a5a01d608 --- /dev/null +++ b/libs/jansson_ext/gtest/src/test_format_check_extended.cpp @@ -0,0 +1,208 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "test_common.h" + +extern "C" { +#include "celix_string_format_check.h" +} + +#include +#include + +struct FormatCase { + const char* format; + const char* value; + bool valid; +}; + +/* ── Date (RFC 3339 full-date) ──────────────────────────────────────────── */ + +TEST(FormatCheckTest, Date) { + static const struct { + const char* value; + bool valid; + } cases[] = { + {"1985-04-12", true}, + {"2020-02-29", true}, /* leap year */ + {"2000-02-29", true}, /* 400-year leap */ + {"0001-01-01", true}, + {"2024-02-29", true}, /* leap year */ + /* ── invalid ── */ + {"1985-4-12", false}, /* no zero-padding (len 9) */ + {"1985-13-01", false}, /* month 13 */ + {"1985-04-31", false}, /* April has 30 days */ + {"2021-02-29", false}, /* non-leap year */ + {"1900-02-29", false}, /* century non-leap */ + {"abcd-ef-gh", false}, /* non-numeric */ + {"", false}, + }; + + for (auto& c : cases) { + int rc = celix_jansson_check_date(c.value); + if (c.valid) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << "Expected valid date: " << c.value; + else + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, rc) << "Expected invalid date: " << c.value; + } +} + +/* ── Time (RFC 3339 full-time) ──────────────────────────────────────────── */ + +TEST(FormatCheckTest, Time) { + static const struct { + const char* value; + bool valid; + } cases[] = { + {"00:00:00Z", true}, + {"12:34:56.123+01:00", true}, + {"23:59:60Z", true}, /* leap second */ + {"00:00:00z", true}, /* lowercase z */ + {"1:34:56Z", true}, /* non-zero-padded: %2d reads up to 2 digits */ + /* ── invalid ── */ + {"24:00:00Z", false}, /* hour > 23 */ + {"12:60:00Z", false}, /* minute > 59 */ + {"12:34:56", false}, /* no timezone */ + {"12:34:56+24:00", false}, /* offset hour > 23 */ + {"10:30:00+01:00x", false}, /* trailing garbage after timezone */ + {"", false}, + }; + + for (auto& c : cases) { + int rc = celix_jansson_check_time(c.value); + if (c.valid) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << "Expected valid time: " << c.value; + else + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, rc) << "Expected invalid time: " << c.value; + } +} + +/* ── Email (via SMTP state machine) ─────────────────────────────────────── + * + * NOTE: The Ragel-generated SMTP address validator rejects many + * RFC 5321-valid addresses — email/idn-email are expected-fail in the + * JSON Schema Test Suite. This test only exercises invalid-input code + * paths that are unambiguous failures. + */ + +TEST(FormatCheckTest, Email) { + /* Valid ASCII address */ + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_check_email("joe.bloggs@example.com")); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_check_email("a@b.com")); + /* Non-ASCII input fails the is_ascii pre-check */ + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, celix_jansson_check_email("\xc3\xbc@example.com")); /* ü@example.com */ + /* Clearly invalid inputs */ + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, celix_jansson_check_email("not-an-email")); + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, celix_jansson_check_email("user@")); + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, celix_jansson_check_email("@example.com")); +} + +/* ── International Email (RFC 6531) ──────────────────────────────────────── */ + +TEST(FormatCheckTest, IdnEmail) { + /* No is_ascii pre-check — UTF-8 addresses are passed to the SMTP validator */ + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_check_idn_email("joe.bloggs@example.com")); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_check_idn_email("\xc3\xbc@example.com")); + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, celix_jansson_check_idn_email("not-an-email")); + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, celix_jansson_check_idn_email("user@")); +} + +/* ── Hostname (RFC 3986 Appendix A labels) ──────────────────────────────── */ + +TEST(FormatCheckTest, Hostname) { + static const struct { + const char* value; + bool valid; + } cases[] = { + {"example.com", true}, + {"localhost", true}, + {"a-b.c-d.example", true}, + {"single", true}, + /* ── invalid ── */ + {"", false}, + {"-bad.example", false}, /* leading hyphen */ + {"bad-.example", false}, /* trailing hyphen in label */ + {"bad..example", false}, /* empty label */ + {"a b.example", false}, /* space in label */ + }; + + /* 4 labels of 63 chars + 3 dots = 255 chars > 253 limit */ + std::string long_label(63, 'a'); + std::string long_host = long_label + "." + long_label + "." + long_label + "." + long_label; + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT, celix_jansson_check_hostname(long_host.c_str())); + + for (auto& c : cases) { + int rc = celix_jansson_check_hostname(c.value); + if (c.valid) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << "Expected valid hostname: " << c.value; + else + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, rc) << "Expected invalid hostname: " << c.value; + } +} + +/* ── IPv6 ───────────────────────────────────────────────────────────────── */ + +TEST(FormatCheckTest, Ipv6) { + static const struct { + const char* value; + bool valid; + } cases[] = { + {"::1", true}, + {"::", true}, + {"2001:db8::1", true}, + {"2001:0db8:0000:0000:0000:0000:0000:0001", true}, + {"::ffff:192.168.1.1", true}, + /* ── invalid ── */ + {"192.168.1.1", false}, /* IPv4, not IPv6 */ + {"2001:db8", false}, /* incomplete */ + {"2001:db8:::1", false}, /* triple colon */ + {"gggg:db8::1", false}, /* invalid hex */ + {"fe80::1%eth0", false}, /* zone-id form — rejected by explicit '%' check */ + {"", false}, + }; + + for (auto& c : cases) { + int rc = celix_jansson_check_ipv6(c.value); + if (c.valid) + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << "Expected valid ipv6: " << c.value; + else + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, rc) << "Expected invalid ipv6: " << c.value; + } +} + +/* ── Unsupported formats ────────────────────────────────────────────────── */ + +TEST(FormatCheckTest, UnsupportedFormats) { + static const char* unsupported[] = { + "uri-reference", + "iri", + "iri-reference", + "idn-hostname", + "json-pointer", + "relative-json-pointer", + "uri-template", + "made-up-format", + nullptr + }; + + for (const char** u = unsupported; *u; u++) { + int rc = celix_jansson_schema_default_format_check(*u, "valid-looking-value", nullptr); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT, rc) + << "Unsupported format '" << *u << "' should return INVALID_ARGUMENT"; + } +} diff --git a/libs/jansson_ext/gtest/src/test_json_patch.cpp b/libs/jansson_ext/gtest/src/test_json_patch.cpp new file mode 100644 index 000000000..eb9841c06 --- /dev/null +++ b/libs/jansson_ext/gtest/src/test_json_patch.cpp @@ -0,0 +1,394 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "celix_json_patch.h" +#include +#include + +/* ── celix_json_patch_add ────────────────────────────────────────────────────── */ + +TEST(JsonPatchTest, AddOperation) { + json_t* patch = json_array(); + json_t* val = json_integer(42); + + EXPECT_EQ(0, celix_json_patch_add(patch, "/x", val)); + EXPECT_EQ(1u, json_array_size(patch)); + + json_t* op = json_array_get(patch, 0); + EXPECT_STREQ("add", json_string_value(json_object_get(op, "op"))); + EXPECT_STREQ("/x", json_string_value(json_object_get(op, "path"))); + EXPECT_EQ(42, json_integer_value(json_object_get(op, "value"))); + + json_decref(patch); +} + +/* ── celix_json_patch_replace ────────────────────────────────────────────────── */ + +TEST(JsonPatchTest, ReplaceOperation) { + json_t* patch = json_array(); + json_t* val = json_string("newval"); + + EXPECT_EQ(0, celix_json_patch_replace(patch, "/a/b", val)); + EXPECT_EQ(1u, json_array_size(patch)); + + json_t* op = json_array_get(patch, 0); + EXPECT_STREQ("replace", json_string_value(json_object_get(op, "op"))); + EXPECT_STREQ("/a/b", json_string_value(json_object_get(op, "path"))); + EXPECT_STREQ("newval", json_string_value(json_object_get(op, "value"))); + + json_decref(patch); +} + +/* ── celix_json_patch_remove ─────────────────────────────────────────────────── */ + +TEST(JsonPatchTest, RemoveOperation) { + json_t* patch = json_array(); + + EXPECT_EQ(0, celix_json_patch_remove(patch, "/old/key")); + EXPECT_EQ(1u, json_array_size(patch)); + + json_t* op = json_array_get(patch, 0); + EXPECT_STREQ("remove", json_string_value(json_object_get(op, "op"))); + EXPECT_STREQ("/old/key", json_string_value(json_object_get(op, "path"))); + /* remove has no "value" field */ + EXPECT_EQ(nullptr, json_object_get(op, "value")); + + json_decref(patch); +} + +/* ── celix_json_patch_truncate ───────────────────────────────────────────────── */ + +TEST(JsonPatchTest, Truncate) { + json_t* patch = json_array(); + celix_json_patch_add(patch, "/a", json_integer(1)); + celix_json_patch_add(patch, "/b", json_integer(2)); + celix_json_patch_add(patch, "/c", json_integer(3)); + EXPECT_EQ(3u, json_array_size(patch)); + + celix_json_patch_truncate(patch, 1); + EXPECT_EQ(1u, json_array_size(patch)); + + json_t* op = json_array_get(patch, 0); + EXPECT_STREQ("/a", json_string_value(json_object_get(op, "path"))); + + json_decref(patch); +} + +/* ── Edge cases: NULL / non-array inputs ──────────────────────────────── */ + +TEST(JsonPatchTest, NullInputs) { + json_t* v1 = json_integer(1); + EXPECT_NE(0, celix_json_patch_add(nullptr, "/x", v1)); + json_decref(v1); + json_t* v2 = json_integer(1); + EXPECT_NE(0, celix_json_patch_replace(nullptr, "/x", v2)); + json_decref(v2); + EXPECT_NE(0, celix_json_patch_remove(nullptr, "/x")); + + json_t* non_array = json_string("not_an_array"); + json_t* v3 = json_integer(1); + EXPECT_NE(0, celix_json_patch_add(non_array, "/x", v3)); + json_decref(v3); + json_t* v4 = json_integer(1); + EXPECT_NE(0, celix_json_patch_replace(non_array, "/x", v4)); + json_decref(v4); + EXPECT_NE(0, celix_json_patch_remove(non_array, "/x")); + json_decref(non_array); + + /* truncate with null/non-array is no-op (doesn't crash) */ + celix_json_patch_truncate(nullptr, 0); + json_t* obj = json_object(); + celix_json_patch_truncate(obj, 5); + json_decref(obj); +} + +/* ── Apply: add at array positions ────────────────────────────────────── */ + +TEST(JsonPatchTest, ApplyAddArrayBegin) { + json_t* doc = json_loads("[10,20]", JSON_DECODE_ANY, nullptr); + json_t* patch = json_array(); + celix_json_patch_add(patch, "/0", json_integer(5)); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + EXPECT_EQ(5, json_integer_value(json_array_get(result, 0))); + EXPECT_EQ(10, json_integer_value(json_array_get(result, 1))); + EXPECT_EQ(20, json_integer_value(json_array_get(result, 2))); + + json_decref(doc); + json_decref(patch); + json_decref(result); +} + +TEST(JsonPatchTest, ApplyAddArrayMiddle) { + json_t* doc = json_loads("[10,20]", JSON_DECODE_ANY, nullptr); + json_t* patch = json_array(); + celix_json_patch_add(patch, "/1", json_integer(15)); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + EXPECT_EQ(10, json_integer_value(json_array_get(result, 0))); + EXPECT_EQ(15, json_integer_value(json_array_get(result, 1))); + EXPECT_EQ(20, json_integer_value(json_array_get(result, 2))); + + json_decref(doc); + json_decref(patch); + json_decref(result); +} + +/* ── Apply: multiple ops in one patch ──────────────────────────────────── */ + +TEST(JsonPatchTest, ApplyMixedOps) { + json_t* doc = json_loads(R"({"users":[{"name":"A"},{"name":"B"}]})", JSON_DECODE_ANY, nullptr); + json_t* patch = json_array(); + celix_json_patch_remove(patch, "/users/1"); + celix_json_patch_replace(patch, "/users/0/name", json_string("Alpha")); + celix_json_patch_add(patch, "/count", json_integer(1)); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + + json_t* expected = json_loads(R"({"users":[{"name":"Alpha"}],"count":1})", JSON_DECODE_ANY, nullptr); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +/* ── Apply: remove last array element ─────────────────────────────────── */ + +TEST(JsonPatchTest, ApplyRemoveLastElement) { + json_t* doc = json_loads("[1,2,3]", JSON_DECODE_ANY, nullptr); + json_t* patch = json_array(); + celix_json_patch_remove(patch, "/2"); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + EXPECT_EQ(2u, json_array_size(result)); + EXPECT_EQ(1, json_integer_value(json_array_get(result, 0))); + EXPECT_EQ(2, json_integer_value(json_array_get(result, 1))); + + json_decref(doc); + json_decref(patch); + json_decref(result); +} + +/* ── Apply: replace nested deep value ─────────────────────────────────── */ + +TEST(JsonPatchTest, ApplyReplaceDeep) { + json_t* doc = json_loads(R"({"a":{"b":{"c":1}}})", JSON_DECODE_ANY, nullptr); + json_t* patch = json_array(); + celix_json_patch_replace(patch, "/a/b/c", json_integer(99)); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + + json_t* expected = json_loads(R"({"a":{"b":{"c":99}}})", JSON_DECODE_ANY, nullptr); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +/* ── Apply: add with numeric key in object path ───────────────────────── */ + +TEST(JsonPatchTest, ApplyAddNumericKey) { + json_t* doc = json_object(); + json_t* patch = json_array(); + celix_json_patch_add(patch, "/123", json_string("numeric key")); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + + json_t* val = json_object_get(result, "123"); + ASSERT_NE(nullptr, val); + EXPECT_STREQ("numeric key", json_string_value(val)); + + json_decref(doc); + json_decref(patch); + json_decref(result); +} + +/* ── Apply: ops with invalid path / missing value are skipped ─────────── */ + +TEST(JsonPatchTest, ApplyAddInvalidPath) { + json_t* doc = json_loads(R"({})", JSON_DECODE_ANY, nullptr); + /* path without leading '/' fails to parse as a JSON Pointer */ + json_t* patch = json_loads(R"([{"op":"add","path":"foo","value":5}])", JSON_DECODE_ANY, nullptr); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + + json_t* expected = json_loads(R"({})", JSON_DECODE_ANY, nullptr); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +TEST(JsonPatchTest, ApplyMissingValue) { + json_t* doc = json_loads(R"({})", JSON_DECODE_ANY, nullptr); + /* add/replace ops without a "value" are skipped, later ops still apply */ + json_t* patch = json_loads(R"([{"op":"add","path":"/a"},{"op":"replace","path":"/b"},{"op":"add","path":"/c","value":1}])", JSON_DECODE_ANY, nullptr); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + + json_t* expected = json_loads(R"({"c":1})", JSON_DECODE_ANY, nullptr); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +/* ── Apply: walk through array parents creating intermediate nodes ────── */ + +TEST(JsonPatchTest, ApplyAddThroughArrayCreatingNode) { + json_t* doc = json_loads(R"({"arr":[]})", JSON_DECODE_ANY, nullptr); + json_t* patch = json_array(); + celix_json_patch_add(patch, "/arr/2/x", json_integer(5)); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + + /* gap filled with nulls, missing element created as object */ + json_t* expected = json_loads(R"({"arr":[null,null,{"x":5}]})", JSON_DECODE_ANY, nullptr); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +TEST(JsonPatchTest, ApplyAddThroughArrayInvalidToken) { + json_t* doc = json_loads(R"({"arr":[1,2]})", JSON_DECODE_ANY, nullptr); + json_t* patch = json_array(); + celix_json_patch_add(patch, "/arr/foo/bar", json_integer(5)); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + + /* non-numeric array token aborts the walk, op is dropped */ + json_t* expected = json_loads(R"({"arr":[1,2]})", JSON_DECODE_ANY, nullptr); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +TEST(JsonPatchTest, ApplyAddBeyondArrayEnd) { + json_t* doc = json_loads(R"({"arr":[10]})", JSON_DECODE_ANY, nullptr); + json_t* patch = json_array(); + celix_json_patch_add(patch, "/arr/3", json_integer(5)); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + + /* gap filled with nulls, value appended at the end */ + json_t* expected = json_loads(R"({"arr":[10,null,null,5]})", JSON_DECODE_ANY, nullptr); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +TEST(JsonPatchTest, ApplyAddThroughScalarParent) { + json_t* doc = json_loads(R"({"a":5})", JSON_DECODE_ANY, nullptr); + json_t* patch = json_array(); + celix_json_patch_add(patch, "/a/b/c", json_integer(1)); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + + /* scalar parent mid-walk → NULL → walk aborted, op is dropped */ + json_t* expected = json_loads(R"({"a":5})", JSON_DECODE_ANY, nullptr); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +/* ── Apply: remove through array parents ──────────────────────────────── */ + +TEST(JsonPatchTest, ApplyRemoveThroughArray) { + json_t* doc = json_loads(R"({"arr":[{"key":1},{"key":2}]})", JSON_DECODE_ANY, nullptr); + json_t* patch = json_array(); + celix_json_patch_remove(patch, "/arr/0/key"); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + + json_t* expected = json_loads(R"({"arr":[{},{"key":2}]})", JSON_DECODE_ANY, nullptr); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +TEST(JsonPatchTest, ApplyRemoveThroughScalar) { + json_t* doc = json_loads(R"({"a":5})", JSON_DECODE_ANY, nullptr); + json_t* patch = json_array(); + celix_json_patch_remove(patch, "/a/b/c"); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + + /* scalar parent mid-walk → NULL → walk aborted, op is dropped */ + json_t* expected = json_loads(R"({"a":5})", JSON_DECODE_ANY, nullptr); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +TEST(JsonPatchTest, ApplyRemoveThroughArrayInvalidIndex) { + json_t* doc = json_loads(R"({"arr":[{"key":1}]})", JSON_DECODE_ANY, nullptr); + json_t* patch = json_array(); + celix_json_patch_remove(patch, "/arr/5/key"); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + + /* out-of-range array index → parent NULL → op is dropped */ + json_t* expected = json_loads(R"({"arr":[{"key":1}]})", JSON_DECODE_ANY, nullptr); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} diff --git a/libs/jansson_ext/gtest/src/test_merge_patch.cpp b/libs/jansson_ext/gtest/src/test_merge_patch.cpp new file mode 100644 index 000000000..8a326d4ac --- /dev/null +++ b/libs/jansson_ext/gtest/src/test_merge_patch.cpp @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "celix_json_merge_patch.h" +#include + +/* ── RFC 7396 examples ───────────────────────────────────────────────────── + * Section 1, Section 3 and Appendix A examples, transcribed from the + * nlohmann/json merge_patch test suite (tests/src/unit-merge_patch.cpp). + * Ex1..Ex15 correspond to RFC 7396 Appendix A.1..A.15; A16 is missing from + * the nlohmann suite and is covered here as well. */ + +static void applyAndExpectEqual(const char* targetText, const char* patchText, const char* expectedText) { + json_auto_t* target = json_loads(targetText, JSON_DECODE_ANY, nullptr); + json_auto_t* patch = json_loads(patchText, JSON_DECODE_ANY, nullptr); + json_auto_t* expected = json_loads(expectedText, JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, target); + ASSERT_NE(nullptr, patch); + ASSERT_NE(nullptr, expected); + + json_auto_t* result = celix_json_merge_patch(target, patch); + ASSERT_NE(nullptr, result); + EXPECT_TRUE(json_equal(result, expected)); +} + +TEST(JsonMergePatchTest, Section1) { + applyAndExpectEqual( + R"({"a":"b","c":{"d":"e","f":"g"}})", + R"({"a":"z","c":{"f":null}})", + R"({"a":"z","c":{"d":"e"}})"); +} + +TEST(JsonMergePatchTest, Section3) { + applyAndExpectEqual( + R"({"title":"Goodbye!","author":{"givenName":"John","familyName":"Doe"},"tags":["example","sample"],"content":"This will be unchanged"})", + R"({"title":"Hello!","phoneNumber":"+01-123-456-7890","author":{"familyName":null},"tags":["example"]})", + R"({"title":"Hello!","author":{"givenName":"John"},"tags":["example"],"content":"This will be unchanged","phoneNumber":"+01-123-456-7890"})"); +} + +TEST(JsonMergePatchTest, Ex1) { + applyAndExpectEqual(R"({"a":"b"})", R"({"a":"c"})", R"({"a":"c"})"); +} + +TEST(JsonMergePatchTest, Ex2) { + applyAndExpectEqual(R"({"a":"b"})", R"({"b":"c"})", R"({"a":"b","b":"c"})"); +} + +TEST(JsonMergePatchTest, Ex3) { + applyAndExpectEqual(R"({"a":"b"})", R"({"a":null})", "{}"); +} + +TEST(JsonMergePatchTest, Ex4) { + applyAndExpectEqual(R"({"a":"b","b":"c"})", R"({"a":null})", R"({"b":"c"})"); +} + +TEST(JsonMergePatchTest, Ex5) { + applyAndExpectEqual(R"({"a":["b"]})", R"({"a":"c"})", R"({"a":"c"})"); +} + +TEST(JsonMergePatchTest, Ex6) { + applyAndExpectEqual(R"({"a":"c"})", R"({"a":["b"]})", R"({"a":["b"]})"); +} + +TEST(JsonMergePatchTest, Ex7) { + applyAndExpectEqual(R"({"a":{"b":"c"}})", R"({"a":{"b":"d","c":null}})", R"({"a":{"b":"d"}})"); +} + +TEST(JsonMergePatchTest, Ex8) { + applyAndExpectEqual(R"({"a":[{"b":"c"}]})", R"({"a":[1]})", R"({"a":[1]})"); +} + +TEST(JsonMergePatchTest, Ex9) { + applyAndExpectEqual(R"(["a","b"])", R"(["c","d"])", R"(["c","d"])"); +} + +TEST(JsonMergePatchTest, Ex10) { + applyAndExpectEqual(R"({"a":"b"})", R"(["c"])", R"(["c"])"); +} + +TEST(JsonMergePatchTest, Ex11) { + applyAndExpectEqual(R"({"a":"foo"})", "null", "null"); +} + +TEST(JsonMergePatchTest, Ex12) { + applyAndExpectEqual(R"({"a":"foo"})", R"("bar")", R"("bar")"); +} + +TEST(JsonMergePatchTest, Ex13) { + applyAndExpectEqual(R"({"e":null})", R"({"a":1})", R"({"e":null,"a":1})"); +} + +TEST(JsonMergePatchTest, Ex14) { + applyAndExpectEqual("[1,2]", R"({"a":"b","c":null})", R"({"a":"b"})"); +} + +TEST(JsonMergePatchTest, Ex15) { + applyAndExpectEqual("{}", R"({"a":{"bb":{"ccc":null}}})", R"({"a":{"bb":{}}})"); +} + +TEST(JsonMergePatchTest, A16) { + /* RFC 7396 Appendix A.16 (absent from the nlohmann suite) */ + applyAndExpectEqual(R"({"a":{"b":{}}})", R"({"a":{"b":null}})", R"({"a":{}})"); +} + +/* ── Edge cases ──────────────────────────────────────────────────────────── */ + +TEST(JsonMergePatchTest, NullArgs) { + json_auto_t* doc = json_loads(R"({"a":1})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + EXPECT_EQ(nullptr, celix_json_merge_patch(nullptr, doc)); + EXPECT_EQ(nullptr, celix_json_merge_patch(doc, nullptr)); + EXPECT_EQ(nullptr, celix_json_merge_patch(nullptr, nullptr)); +} + +TEST(JsonMergePatchTest, EmptyPatch) { + applyAndExpectEqual(R"({"a":1})", "{}", R"({"a":1})"); + /* an object patch converts a non-object target to {} even with no members */ + applyAndExpectEqual("[1,2]", "{}", "{}"); +} + +TEST(JsonMergePatchTest, AddMember) { + applyAndExpectEqual("{}", R"({"a":"b"})", R"({"a":"b"})"); +} + +TEST(JsonMergePatchTest, ScalarAndBoolPatch) { + applyAndExpectEqual(R"({"a":1})", "true", "true"); + applyAndExpectEqual(R"({"a":1})", "42", "42"); +} + +TEST(JsonMergePatchTest, DeepNesting) { + applyAndExpectEqual( + R"({"a":{"b":{"c":1},"x":[1,{"y":2}]}})", + R"({"a":{"b":{"d":2},"x":[9]}})", + R"({"a":{"b":{"c":1,"d":2},"x":[9]}})"); +} + +TEST(JsonMergePatchTest, InputsUnmodifiedAndNewDocument) { + json_auto_t* target = json_loads(R"({"a":{"b":1},"c":[1,2]})", JSON_DECODE_ANY, nullptr); + json_auto_t* patch = json_loads(R"({"a":{"b":2}})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, target); + ASSERT_NE(nullptr, patch); + json_auto_t* targetCopy = json_deep_copy(target); + json_auto_t* patchCopy = json_deep_copy(patch); + ASSERT_NE(nullptr, targetCopy); + ASSERT_NE(nullptr, patchCopy); + + json_auto_t* result = celix_json_merge_patch(target, patch); + ASSERT_NE(nullptr, result); + + /* a new document is returned and neither input is modified */ + EXPECT_NE(result, target); + EXPECT_NE(result, patch); + EXPECT_TRUE(json_equal(target, targetCopy)); + EXPECT_TRUE(json_equal(patch, patchCopy)); +} + +TEST(JsonMergePatchTest, SelfMerge) { + /* self-merge with no null members is the identity */ + json_auto_t* doc = json_loads(R"({"a":{"b":1},"c":[1,2]})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + json_auto_t* copy = json_deep_copy(doc); + ASSERT_NE(nullptr, copy); + + json_auto_t* result = celix_json_merge_patch(doc, doc); + ASSERT_NE(nullptr, result); + EXPECT_TRUE(json_equal(result, copy)); +} + +TEST(JsonMergePatchTest, PatchIsTargetSubobject) { + /* the patch may alias a sub-object of the target: RFC 7396 treats the + * patch as a document of its own, so its members are merged at the + * top level */ + json_auto_t* target = json_loads(R"({"a":{"x":1,"y":2}})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, target); + json_t* sub = json_object_get(target, "a"); + ASSERT_NE(nullptr, sub); + json_auto_t* targetCopy = json_deep_copy(target); + ASSERT_NE(nullptr, targetCopy); + + json_auto_t* result = celix_json_merge_patch(target, sub); + ASSERT_NE(nullptr, result); + json_auto_t* expected = json_loads(R"({"a":{"x":1,"y":2},"x":1,"y":2})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, expected); + EXPECT_TRUE(json_equal(result, expected)); + /* the target itself is untouched */ + EXPECT_TRUE(json_equal(target, targetCopy)); +} diff --git a/libs/jansson_ext/gtest/src/test_patch.cpp b/libs/jansson_ext/gtest/src/test_patch.cpp new file mode 100644 index 000000000..3a9f0b11d --- /dev/null +++ b/libs/jansson_ext/gtest/src/test_patch.cpp @@ -0,0 +1,489 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "celix_jansson_schema.h" +#include "celix_json_patch.h" +#include +#include + +/* ── Patch Apply: add operations ──────────────────────────────────────── */ + +TEST(PatchApplyTest, AddRoot) { + json_t* doc = json_loads("42", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + json_t* patch = json_array(); + json_t* op = json_object(); + json_object_set_new(op, "op", json_string("add")); + json_object_set_new(op, "path", json_string("")); + json_object_set_new(op, "value", json_integer(99)); + json_array_append_new(patch, op); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + EXPECT_EQ(99, json_integer_value(result)); + + json_decref(doc); + json_decref(patch); + json_decref(result); +} + +TEST(PatchApplyTest, AddToObject) { + json_t* doc = json_loads(R"({"a":1})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + json_t* patch = json_array(); + json_t* op = json_object(); + json_object_set_new(op, "op", json_string("add")); + json_object_set_new(op, "path", json_string("/b")); + json_object_set_new(op, "value", json_integer(2)); + json_array_append_new(patch, op); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + json_t* expected = json_loads(R"({"a":1,"b":2})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, expected); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +TEST(PatchApplyTest, AddNested) { + json_t* doc = json_loads(R"({"x":{}})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + json_t* patch = json_array(); + json_t* op = json_object(); + json_object_set_new(op, "op", json_string("add")); + json_object_set_new(op, "path", json_string("/x/y/z")); + json_object_set_new(op, "value", json_string("deep")); + json_array_append_new(patch, op); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + json_t* expected = json_loads(R"({"x":{"y":{"z":"deep"}}})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, expected); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +/* ── Patch Apply: replace operations ──────────────────────────────────── */ + +TEST(PatchApplyTest, ReplaceExisting) { + json_t* doc = json_loads(R"({"a":1,"b":2})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + json_t* patch = json_array(); + json_t* op = json_object(); + json_object_set_new(op, "op", json_string("replace")); + json_object_set_new(op, "path", json_string("/b")); + json_object_set_new(op, "value", json_integer(99)); + json_array_append_new(patch, op); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + json_t* expected = json_loads(R"({"a":1,"b":99})", JSON_DECODE_ANY, nullptr); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +TEST(PatchApplyTest, ReplaceInArray) { + json_t* doc = json_loads(R"({"arr":[10,20,30]})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + json_t* patch = json_array(); + json_t* op = json_object(); + json_object_set_new(op, "op", json_string("replace")); + json_object_set_new(op, "path", json_string("/arr/1")); + json_object_set_new(op, "value", json_integer(99)); + json_array_append_new(patch, op); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + json_t* expected = json_loads(R"({"arr":[10,99,30]})", JSON_DECODE_ANY, nullptr); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +TEST(PatchApplyTest, ReplaceRoot) { + json_t* doc = json_string("hello"); + json_t* patch = json_array(); + json_t* op = json_object(); + json_object_set_new(op, "op", json_string("replace")); + json_object_set_new(op, "path", json_string("")); + json_object_set_new(op, "value", json_string("world")); + json_array_append_new(patch, op); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + EXPECT_STREQ("world", json_string_value(result)); + + json_decref(doc); + json_decref(patch); + json_decref(result); +} + +/* ── Patch Apply: remove operations ───────────────────────────────────── */ + +TEST(PatchApplyTest, RemoveKey) { + json_t* doc = json_loads(R"({"a":1,"b":2,"c":3})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + json_t* patch = json_array(); + json_t* op = json_object(); + json_object_set_new(op, "op", json_string("remove")); + json_object_set_new(op, "path", json_string("/b")); + json_array_append_new(patch, op); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + json_t* expected = json_loads(R"({"a":1,"c":3})", JSON_DECODE_ANY, nullptr); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +TEST(PatchApplyTest, RemoveFromArray) { + json_t* doc = json_loads(R"({"arr":[10,20,30]})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + json_t* patch = json_array(); + json_t* op = json_object(); + json_object_set_new(op, "op", json_string("remove")); + json_object_set_new(op, "path", json_string("/arr/1")); + json_array_append_new(patch, op); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + json_t* expected = json_loads(R"({"arr":[10,30]})", JSON_DECODE_ANY, nullptr); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +TEST(PatchApplyTest, RemoveRoot) { + json_t* doc = json_integer(42); + json_t* patch = json_array(); + json_t* op = json_object(); + json_object_set_new(op, "op", json_string("remove")); + json_object_set_new(op, "path", json_string("")); + json_array_append_new(patch, op); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + EXPECT_TRUE(json_is_null(result)); + + json_decref(doc); + json_decref(patch); + json_decref(result); +} + +/* ── Patch Apply: multiple operations ──────────────────────────────────── */ + +TEST(PatchApplyTest, MultipleOperations) { + json_t* doc = json_loads(R"({"a":1,"b":2})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + json_t* patch = json_array(); + + /* Add /c */ + json_t* op1 = json_object(); + json_object_set_new(op1, "op", json_string("add")); + json_object_set_new(op1, "path", json_string("/c")); + json_object_set_new(op1, "value", json_integer(3)); + json_array_append_new(patch, op1); + + /* Replace /a */ + json_t* op2 = json_object(); + json_object_set_new(op2, "op", json_string("replace")); + json_object_set_new(op2, "path", json_string("/a")); + json_object_set_new(op2, "value", json_integer(99)); + json_array_append_new(patch, op2); + + /* Remove /b */ + json_t* op3 = json_object(); + json_object_set_new(op3, "op", json_string("remove")); + json_object_set_new(op3, "path", json_string("/b")); + json_array_append_new(patch, op3); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + json_t* expected = json_loads(R"({"a":99,"c":3})", JSON_DECODE_ANY, nullptr); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +TEST(PatchApplyTest, SequenceDependent) { + json_t* doc = json_loads(R"({"a":[1,2,3]})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + json_t* patch = json_array(); + /* Remove /a/1 first (removes 2), then the array is [1,3] */ + json_t* op1 = json_object(); + json_object_set_new(op1, "op", json_string("remove")); + json_object_set_new(op1, "path", json_string("/a/1")); + json_array_append_new(patch, op1); + + /* Replace /a/1 (now points to 3) with 99 */ + json_t* op2 = json_object(); + json_object_set_new(op2, "op", json_string("replace")); + json_object_set_new(op2, "path", json_string("/a/1")); + json_object_set_new(op2, "value", json_integer(99)); + json_array_append_new(patch, op2); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + json_t* expected = json_loads(R"({"a":[1,99]})", JSON_DECODE_ANY, nullptr); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +/* ── Edge cases ────────────────────────────────────────────────────────── */ + +TEST(PatchApplyTest, NullDocReturnsNull) { + json_t* patch = json_array(); + json_t* result = celix_json_patch_apply(nullptr, patch); + EXPECT_EQ(nullptr, result); + json_decref(patch); +} + +TEST(PatchApplyTest, NullPatchReturnsNull) { + json_t* doc = json_integer(1); + json_t* result = celix_json_patch_apply(doc, nullptr); + EXPECT_EQ(nullptr, result); + json_decref(doc); +} + +TEST(PatchApplyTest, NonArrayPatchReturnsNull) { + json_t* doc = json_integer(1); + json_t* patch = json_string("bad"); + json_t* result = celix_json_patch_apply(doc, patch); + EXPECT_EQ(nullptr, result); + json_decref(doc); + json_decref(patch); +} + +TEST(PatchApplyTest, EmptyPatchReturnsCopy) { + json_t* doc = json_loads(R"({"a":1})", JSON_DECODE_ANY, nullptr); + json_t* patch = json_array(); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + EXPECT_TRUE(json_equal(doc, result)); + EXPECT_NE(doc, result); /* must be a copy, not the same object */ + + json_decref(doc); + json_decref(patch); + json_decref(result); +} + +TEST(PatchApplyTest, UnknownOpSkipped) { + json_t* doc = json_object(); + json_t* patch = json_array(); + json_t* op = json_object(); + json_object_set_new(op, "op", json_string("move")); + json_object_set_new(op, "path", json_string("/a")); + json_object_set_new(op, "value", json_integer(1)); + json_array_append_new(patch, op); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + json_t* expected = json_object(); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +TEST(PatchApplyTest, MissingOpField) { + json_t* doc = json_object(); + json_t* patch = json_array(); + json_t* op = json_object(); + json_object_set_new(op, "path", json_string("/a")); + json_object_set_new(op, "value", json_integer(1)); + json_array_append_new(patch, op); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + /* No op field → skipped */ + EXPECT_TRUE(json_equal(result, doc)); + + json_decref(doc); + json_decref(patch); + json_decref(result); +} + +TEST(PatchApplyTest, MissingPathField) { + json_t* doc = json_loads(R"({"a":1})", JSON_DECODE_ANY, nullptr); + json_t* patch = json_array(); + json_t* op = json_object(); + json_object_set_new(op, "op", json_string("remove")); + json_array_append_new(patch, op); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + EXPECT_TRUE(json_equal(result, doc)); + + json_decref(doc); + json_decref(patch); + json_decref(result); +} + +/* ── Array-specific edge cases ─────────────────────────────────────────── */ + +TEST(PatchApplyTest, AddToArrayIndex) { + json_t* doc = json_loads(R"({"arr":[1,2]})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + json_t* patch = json_array(); + json_t* op = json_object(); + json_object_set_new(op, "op", json_string("add")); + json_object_set_new(op, "path", json_string("/arr/1")); + json_object_set_new(op, "value", json_integer(99)); + json_array_append_new(patch, op); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + json_t* expected = json_loads(R"({"arr":[1,99,2]})", JSON_DECODE_ANY, nullptr); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +TEST(PatchApplyTest, AddToArrayAppend) { + json_t* doc = json_loads(R"({"arr":[1,2]})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + json_t* patch = json_array(); + json_t* op = json_object(); + json_object_set_new(op, "op", json_string("add")); + json_object_set_new(op, "path", json_string("/arr/2")); + json_object_set_new(op, "value", json_integer(3)); + json_array_append_new(patch, op); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + json_t* expected = json_loads(R"({"arr":[1,2,3]})", JSON_DECODE_ANY, nullptr); + EXPECT_TRUE(json_equal(result, expected)); + + json_decref(doc); + json_decref(patch); + json_decref(result); + json_decref(expected); +} + +TEST(PatchApplyTest, ReplaceNonExistentInArray) { + json_t* doc = json_loads(R"({"arr":[1]})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + json_t* patch = json_array(); + json_t* op = json_object(); + json_object_set_new(op, "op", json_string("replace")); + json_object_set_new(op, "path", json_string("/arr/5")); + json_object_set_new(op, "value", json_integer(99)); + json_array_append_new(patch, op); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + /* Non-existent index → operation skipped, document unchanged */ + EXPECT_TRUE(json_equal(result, doc)); + + json_decref(doc); + json_decref(patch); + json_decref(result); +} + +/* ── Original unchanged ────────────────────────────────────────────────── */ + +TEST(PatchApplyTest, OriginalUnchanged) { + json_t* doc = json_loads(R"({"a":1})", JSON_DECODE_ANY, nullptr); + json_t* patch = json_array(); + json_t* op = json_object(); + json_object_set_new(op, "op", json_string("add")); + json_object_set_new(op, "path", json_string("/b")); + json_object_set_new(op, "value", json_integer(2)); + json_array_append_new(patch, op); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + EXPECT_FALSE(json_equal(doc, result)); /* doc should be unchanged */ + + /* doc still has only "a" */ + json_t* a = json_object_get(doc, "a"); + ASSERT_NE(nullptr, a); + EXPECT_EQ(1, json_integer_value(a)); + EXPECT_EQ(nullptr, json_object_get(doc, "b")); + + json_decref(doc); + json_decref(patch); + json_decref(result); +} + +/* ── Remove array element at non-numeric index ─────────────────────────── */ + +TEST(PatchApplyTest, RemoveNonNumericArrayIndex) { + json_t* doc = json_loads(R"({"arr":[1,2,3]})", JSON_DECODE_ANY, nullptr); + json_t* patch = json_array(); + json_t* op = json_object(); + json_object_set_new(op, "op", json_string("remove")); + json_object_set_new(op, "path", json_string("/arr/one")); + json_array_append_new(patch, op); + + json_t* result = celix_json_patch_apply(doc, patch); + ASSERT_NE(nullptr, result); + /* Non-numeric index → skip, doc unchanged */ + EXPECT_TRUE(json_equal(result, doc)); + + json_decref(doc); + json_decref(patch); + json_decref(result); +} diff --git a/libs/jansson_ext/gtest/src/test_path.cpp b/libs/jansson_ext/gtest/src/test_path.cpp new file mode 100644 index 000000000..db1338212 --- /dev/null +++ b/libs/jansson_ext/gtest/src/test_path.cpp @@ -0,0 +1,248 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +extern "C" { +#include "celix_schema.h" +} + +#include + +/* ── init ────────────────────────────────────────────────────────────────── */ + +TEST(PathTest, Init) { + celix_jansson_path_t p; + celix_jansson_path_init(&p); + + EXPECT_EQ(0, p.len); + EXPECT_EQ(0, p.cap); + EXPECT_STREQ("", celix_jansson_path_str(&p)); + + celix_jansson_path_free(&p); +} + +/* ── push ────────────────────────────────────────────────────────────────── */ + +TEST(PathTest, PushSingle) { + celix_jansson_path_t p; + celix_jansson_path_init(&p); + + int rc = celix_jansson_path_push(&p, "token"); + EXPECT_EQ(0, rc); + EXPECT_EQ(1, p.len); + EXPECT_STREQ("/token", celix_jansson_path_str(&p)); + + celix_jansson_path_free(&p); +} + +TEST(PathTest, PushMultiple) { + celix_jansson_path_t p; + celix_jansson_path_init(&p); + + EXPECT_EQ(0, celix_jansson_path_push(&p, "first")); + EXPECT_EQ(0, celix_jansson_path_push(&p, "second")); + EXPECT_EQ(0, celix_jansson_path_push(&p, "third")); + EXPECT_EQ(3, p.len); + EXPECT_STREQ("/first/second/third", celix_jansson_path_str(&p)); + + celix_jansson_path_free(&p); +} + +TEST(PathTest, PushTokenWithSpecialChars) { + celix_jansson_path_t p; + celix_jansson_path_init(&p); + + /* raw tokens are stored as-is; escaping is tested under str */ + EXPECT_EQ(0, celix_jansson_path_push(&p, "a/b")); + EXPECT_EQ(0, celix_jansson_path_push(&p, "c~d")); + EXPECT_EQ(2, p.len); + EXPECT_STREQ("/a~1b/c~0d", celix_jansson_path_str(&p)); + + celix_jansson_path_free(&p); +} + +/* ── pop ─────────────────────────────────────────────────────────────────── */ + +TEST(PathTest, PopFromEmpty) { + celix_jansson_path_t p; + celix_jansson_path_init(&p); + + /* should be a safe no-op */ + celix_jansson_path_pop(&p); + EXPECT_EQ(0, p.len); + EXPECT_STREQ("", celix_jansson_path_str(&p)); + + celix_jansson_path_free(&p); +} + +TEST(PathTest, PushThenPop) { + celix_jansson_path_t p; + celix_jansson_path_init(&p); + + celix_jansson_path_push(&p, "token"); + celix_jansson_path_pop(&p); + EXPECT_EQ(0, p.len); + EXPECT_STREQ("", celix_jansson_path_str(&p)); + + celix_jansson_path_free(&p); +} + +TEST(PathTest, PushTwoPopOne) { + celix_jansson_path_t p; + celix_jansson_path_init(&p); + + celix_jansson_path_push(&p, "first"); + celix_jansson_path_push(&p, "second"); + celix_jansson_path_pop(&p); + EXPECT_EQ(1, p.len); + EXPECT_STREQ("/first", celix_jansson_path_str(&p)); + + celix_jansson_path_free(&p); +} + +/* ── str (JSON Pointer serialization) ────────────────────────────────────── */ + +TEST(PathTest, StrEmpty) { + celix_jansson_path_t p; + celix_jansson_path_init(&p); + + EXPECT_STREQ("", celix_jansson_path_str(&p)); + + celix_jansson_path_free(&p); +} + +TEST(PathTest, StrSingle) { + celix_jansson_path_t p; + celix_jansson_path_init(&p); + + celix_jansson_path_push(&p, "hello"); + EXPECT_STREQ("/hello", celix_jansson_path_str(&p)); + + celix_jansson_path_free(&p); +} + +TEST(PathTest, StrEscapingTilde) { + celix_jansson_path_t p; + celix_jansson_path_init(&p); + + celix_jansson_path_push(&p, "~"); + EXPECT_STREQ("/~0", celix_jansson_path_str(&p)); + + celix_jansson_path_free(&p); +} + +TEST(PathTest, StrEscapingSlash) { + celix_jansson_path_t p; + celix_jansson_path_init(&p); + + celix_jansson_path_push(&p, "/"); + EXPECT_STREQ("/~1", celix_jansson_path_str(&p)); + + celix_jansson_path_free(&p); +} + +TEST(PathTest, StrEscapingCombined) { + celix_jansson_path_t p; + celix_jansson_path_init(&p); + + celix_jansson_path_push(&p, "a/b"); + celix_jansson_path_push(&p, "c~d"); + EXPECT_STREQ("/a~1b/c~0d", celix_jansson_path_str(&p)); + + celix_jansson_path_free(&p); +} + +TEST(PathTest, StrCacheStable) { + celix_jansson_path_t p; + celix_jansson_path_init(&p); + + celix_jansson_path_push(&p, "x"); + const char* s1 = celix_jansson_path_str(&p); + const char* s2 = celix_jansson_path_str(&p); + EXPECT_EQ(s1, s2); /* same pointer — cache is valid */ + + celix_jansson_path_free(&p); +} + +TEST(PathTest, StrCacheInvalidatedOnPush) { + celix_jansson_path_t p; + celix_jansson_path_init(&p); + celix_jansson_path_push(&p, "x"); + const char* s1 = celix_jansson_path_str(&p); + std::string before = s1; // 失效前拷贝内容 + celix_jansson_path_push(&p, "y"); // 此后 s1 悬垂,但 before 仍有效 + const char* s2 = celix_jansson_path_str(&p); + EXPECT_STREQ(before.c_str(), "/x"); + EXPECT_STREQ(s2, "/x/y"); + celix_jansson_path_free(&p); +} + +TEST(PathTest, StrCacheInvalidatedOnPop) { + celix_jansson_path_t p; + celix_jansson_path_init(&p); + + celix_jansson_path_push(&p, "x"); + celix_jansson_path_push(&p, "y"); + EXPECT_STREQ("/x/y", celix_jansson_path_str(&p)); + celix_jansson_path_pop(&p); + /* Cache was invalidated: a stale cache would still return "/x/y". */ + EXPECT_STREQ("/x", celix_jansson_path_str(&p)); + + celix_jansson_path_free(&p); +} + +/* ── free ────────────────────────────────────────────────────────────────── */ + +TEST(PathTest, FreeEmpty) { + celix_jansson_path_t p; + celix_jansson_path_init(&p); + + celix_jansson_path_free(&p); + /* no crash = pass */ + EXPECT_EQ(0, p.len); + EXPECT_EQ(0, p.cap); +} + +TEST(PathTest, FreeWithTokens) { + celix_jansson_path_t p; + celix_jansson_path_init(&p); + + celix_jansson_path_push(&p, "a"); + celix_jansson_path_push(&p, "b"); + celix_jansson_path_free(&p); + /* no crash = pass */ + EXPECT_EQ(0, p.len); + EXPECT_EQ(0, p.cap); +} + +TEST(PathTest, FreeThenReinit) { + celix_jansson_path_t p; + celix_jansson_path_init(&p); + + celix_jansson_path_push(&p, "a"); + celix_jansson_path_free(&p); + + /* struct is zeroed — re-init and use again */ + celix_jansson_path_init(&p); + EXPECT_EQ(0, p.len); + EXPECT_STREQ("", celix_jansson_path_str(&p)); + celix_jansson_path_push(&p, "b"); + EXPECT_STREQ("/b", celix_jansson_path_str(&p)); + + celix_jansson_path_free(&p); +} diff --git a/libs/jansson_ext/gtest/src/test_pointer.cpp b/libs/jansson_ext/gtest/src/test_pointer.cpp new file mode 100644 index 000000000..0a799aa04 --- /dev/null +++ b/libs/jansson_ext/gtest/src/test_pointer.cpp @@ -0,0 +1,1587 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "celix_jansson_pointer.h" +#include +#include +#include + +/* ── Construction ──────────────────────────────────────────────────────── */ + +TEST(PointerTest, NewEmpty) { + celix_json_pointer_t* p = celix_json_pointer_create(""); + ASSERT_NE(nullptr, p); + EXPECT_EQ(0u, celix_json_pointer_depth(p)); + char* s = celix_json_pointer_to_string(p); + EXPECT_STREQ("/", s); + free(s); + celix_json_pointer_destroy(p); +} + +TEST(PointerTest, NullPointer) { + celix_json_pointer_t* p = celix_json_pointer_create(NULL); + ASSERT_NE(nullptr, p); + EXPECT_EQ(0u, celix_json_pointer_depth(p)); + celix_json_pointer_destroy(p); +} + +TEST(PointerTest, SimplePath) { + celix_json_pointer_t* p = celix_json_pointer_create("/foo/bar"); + ASSERT_NE(nullptr, p); + EXPECT_EQ(2u, celix_json_pointer_depth(p)); + EXPECT_STREQ("foo", celix_json_pointer_token(p, 0)); + EXPECT_STREQ("bar", celix_json_pointer_token(p, 1)); + EXPECT_EQ(nullptr, celix_json_pointer_token(p, 2)); + celix_json_pointer_destroy(p); +} + +TEST(PointerTest, ArrayIndex) { + celix_json_pointer_t* p = celix_json_pointer_create("/store/book/0/title"); + ASSERT_NE(nullptr, p); + EXPECT_EQ(4u, celix_json_pointer_depth(p)); + EXPECT_STREQ("store", celix_json_pointer_token(p, 0)); + EXPECT_STREQ("book", celix_json_pointer_token(p, 1)); + EXPECT_STREQ("0", celix_json_pointer_token(p, 2)); + EXPECT_STREQ("title", celix_json_pointer_token(p, 3)); + celix_json_pointer_destroy(p); +} + +TEST(PointerTest, MustStartWithSlash) { + celix_json_pointer_t* p = celix_json_pointer_create("foo/bar"); + EXPECT_EQ(nullptr, p) << "Pointer without leading '/' should fail"; +} + +/* ── Escaping ──────────────────────────────────────────────────────────── */ + +TEST(PointerTest, EscapeTilde) { + celix_json_pointer_t* p = celix_json_pointer_create("/~0foo/~1bar"); + ASSERT_NE(nullptr, p); + EXPECT_STREQ("~foo", celix_json_pointer_token(p, 0)); + EXPECT_STREQ("/bar", celix_json_pointer_token(p, 1)); + celix_json_pointer_destroy(p); +} + +TEST(PointerTest, EscapeUtilities) { + char* escaped = celix_json_pointer_escape("a/b~c"); + EXPECT_STREQ("a~1b~0c", escaped); + free(escaped); + + char* unescaped = celix_json_pointer_unescape("a~1b~0c"); + EXPECT_STREQ("a/b~c", unescaped); + free(unescaped); +} + +/* ── Round-trip ────────────────────────────────────────────────────────── */ + +TEST(PointerTest, RoundTrip) { + const char* orig = "/foo~0bar/baz~1qux/x/y/z"; + celix_json_pointer_t* p = celix_json_pointer_create(orig); + ASSERT_NE(nullptr, p); + + char* serialized = celix_json_pointer_to_string(p); + EXPECT_STREQ(orig, serialized); + + /* Parse the serialized form again — should be identical */ + celix_json_pointer_t* p2 = celix_json_pointer_create(serialized); + ASSERT_NE(nullptr, p2); + EXPECT_EQ(celix_json_pointer_depth(p), celix_json_pointer_depth(p2)); + EXPECT_TRUE(celix_json_pointer_equals(p, p2)); + + free(serialized); + celix_json_pointer_destroy(p); + celix_json_pointer_destroy(p2); +} + +/* ── Mutation ──────────────────────────────────────────────────────────── */ + +TEST(PointerTest, PushAndPop) { + celix_json_pointer_t* p = celix_json_pointer_create("/a"); + ASSERT_NE(nullptr, p); + EXPECT_EQ(1u, celix_json_pointer_depth(p)); + + celix_json_pointer_push(p, "b"); + EXPECT_EQ(2u, celix_json_pointer_depth(p)); + EXPECT_STREQ("b", celix_json_pointer_token(p, 1)); + + celix_json_pointer_push(p, "c"); + EXPECT_EQ(3u, celix_json_pointer_depth(p)); + + celix_json_pointer_pop(p); + EXPECT_EQ(2u, celix_json_pointer_depth(p)); + EXPECT_STREQ("b", celix_json_pointer_token(p, 1)); + + celix_json_pointer_pop(p); + EXPECT_EQ(1u, celix_json_pointer_depth(p)); + + celix_json_pointer_pop(p); + celix_json_pointer_pop(p); /* extra pop should be safe */ + EXPECT_EQ(0u, celix_json_pointer_depth(p)); + + celix_json_pointer_destroy(p); +} + +/* ── Document resolution ───────────────────────────────────────────────── */ + +TEST(PointerTest, ResolveObject) { + json_t* doc = json_loads(R"({"foo":{"bar":[1,2,3]}})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t* p = celix_json_pointer_create("/foo/bar/0"); + ASSERT_NE(nullptr, p); + + json_t* val = celix_json_pointer_get(doc, p); + ASSERT_NE(nullptr, val); + EXPECT_TRUE(json_is_integer(val)); + EXPECT_EQ(1, json_integer_value(val)); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, ResolveNotFound) { + json_t* doc = json_loads(R"({"a":1})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t* p = celix_json_pointer_create("/b"); + ASSERT_NE(nullptr, p); + EXPECT_EQ(nullptr, celix_json_pointer_get(doc, p)); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, RootPointer) { + json_t* doc = json_loads("42", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t* p = celix_json_pointer_create(""); + ASSERT_NE(nullptr, p); + + json_t* val = celix_json_pointer_get(doc, p); + ASSERT_NE(nullptr, val); + EXPECT_TRUE(json_equal(doc, val)); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +/* ── Set ───────────────────────────────────────────────────────────────── */ + +TEST(PointerTest, SetSimple) { + json_t* doc = json_object(); + celix_json_pointer_t* p = celix_json_pointer_create("/a/b"); + ASSERT_NE(nullptr, p); + + EXPECT_EQ(0, celix_json_pointer_set(doc, p, json_integer(42))); + + /* Verify */ + json_t* v = celix_json_pointer_get(doc, p); + ASSERT_NE(nullptr, v); + EXPECT_EQ(42, json_integer_value(v)); + + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ("{\"a\":{\"b\":42}}", s); + free(s); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, SetArrayIndex) { + json_t* doc = json_object(); + celix_json_pointer_t* p = celix_json_pointer_create("/arr/2"); + ASSERT_NE(nullptr, p); + + EXPECT_EQ(0, celix_json_pointer_set(doc, p, json_string("hello"))); + + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ("{\"arr\":[null,null,\"hello\"]}", s); + free(s); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, SetArrayAppend) { + json_t* doc = json_loads(R"({"a":[1,2]})", JSON_DECODE_ANY, nullptr); + celix_json_pointer_t* p = celix_json_pointer_create("/a/-"); + ASSERT_NE(nullptr, p); + + EXPECT_EQ(0, celix_json_pointer_set(doc, p, json_integer(3))); + + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ("{\"a\":[1,2,3]}", s); + free(s); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, SetArrayIndexNullValue) { + json_t* doc = json_loads("[1,2]", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + celix_json_pointer_t* p = celix_json_pointer_create("/0"); + ASSERT_NE(nullptr, p); + + /* A NULL value cannot be inserted into an array; fails without modifying the document */ + EXPECT_EQ(-1, celix_json_pointer_set(doc, p, nullptr)); + + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ("[1,2]", s); + free(s); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, SetArraySelfReferenceFails) { + json_t* doc = json_loads("[1,2]", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + celix_json_pointer_t* p = celix_json_pointer_create("/0"); + ASSERT_NE(nullptr, p); + json_incref(doc); /* set consumes a reference on failure — keep doc alive */ + + /* An array cannot be inserted into itself; fails without modifying the document */ + EXPECT_EQ(-1, celix_json_pointer_set(doc, p, doc)); + + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ("[1,2]", s); + free(s); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +/* ── Remove ────────────────────────────────────────────────────────────── */ + +TEST(PointerTest, RemoveKey) { + json_t* doc = json_loads(R"({"a":1,"b":2})", JSON_DECODE_ANY, nullptr); + celix_json_pointer_t* p = celix_json_pointer_create("/b"); + ASSERT_NE(nullptr, p); + + EXPECT_EQ(0, celix_json_pointer_remove(doc, p)); + + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ("{\"a\":1}", s); + free(s); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, RemoveArrayElement) { + json_t* doc = json_loads(R"({"arr":[1,2,3]})", JSON_DECODE_ANY, nullptr); + celix_json_pointer_t* p = celix_json_pointer_create("/arr/1"); + ASSERT_NE(nullptr, p); + + EXPECT_EQ(0, celix_json_pointer_remove(doc, p)); + + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ("{\"arr\":[1,3]}", s); + free(s); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +/* ── Parent ────────────────────────────────────────────────────────────── */ + +TEST(PointerTest, Parent) { + celix_json_pointer_t* p = celix_json_pointer_create("/a/b/c"); + ASSERT_NE(nullptr, p); + + celix_json_pointer_t parent; + memset(&parent, 0, sizeof(parent)); + celix_json_pointer_t* pp = celix_json_pointer_parent(p, &parent); + ASSERT_NE(nullptr, pp); + + char* s = celix_json_pointer_to_string(pp); + EXPECT_STREQ("/a/b", s); + free(s); + + /* Parent of parent */ + celix_json_pointer_t grandparent; + memset(&grandparent, 0, sizeof(grandparent)); + celix_json_pointer_t* gp = celix_json_pointer_parent(pp, &grandparent); + ASSERT_NE(nullptr, gp); + + s = celix_json_pointer_to_string(gp); + EXPECT_STREQ("/a", s); + free(s); + + /* Parent of single token → root (empty) */ + celix_json_pointer_t root; + memset(&root, 0, sizeof(root)); + celix_json_pointer_t* rp = celix_json_pointer_parent(gp, &root); + ASSERT_NE(nullptr, rp); + EXPECT_EQ(0u, celix_json_pointer_depth(rp)); /* empty pointer is the root */ + + celix_json_pointer_clear(&parent); + celix_json_pointer_clear(&grandparent); + celix_json_pointer_destroy(p); +} + +TEST(PointerTest, ParentReusesLiveOut) { + /* A caller-provided out that already holds data is cleared before being + * filled — reusing it directly must not leak or mis-fill */ + celix_json_pointer_t p; + memset(&p, 0, sizeof(p)); + celix_json_pointer_t out; + memset(&out, 0, sizeof(out)); + ASSERT_EQ(0, celix_json_pointer_init(&p, "/a/b/c")); + ASSERT_EQ(0, celix_json_pointer_init(&out, "/x/y/z")); /* live data */ + + celix_json_pointer_t* r = celix_json_pointer_parent(&p, &out); + ASSERT_NE(nullptr, r); + EXPECT_EQ(2u, celix_json_pointer_depth(r)); + EXPECT_STREQ("a", celix_json_pointer_token(r, 0)); + EXPECT_STREQ("b", celix_json_pointer_token(r, 1)); + + char* s = celix_json_pointer_to_string(r); + EXPECT_STREQ("/a/b", s); + free(s); + + celix_json_pointer_clear(&out); + celix_json_pointer_clear(&p); +} + +/* ── Concat ────────────────────────────────────────────────────────────── */ + +TEST(PointerTest, Concat) { + celix_json_pointer_t* p = celix_json_pointer_create("/a"); + celix_json_pointer_t* suffix = celix_json_pointer_create("/b/c"); + + ASSERT_EQ(0, celix_json_pointer_concat(p, suffix)); + + char* s = celix_json_pointer_to_string(p); + EXPECT_STREQ("/a/b/c", s); + free(s); + + celix_json_pointer_destroy(p); + celix_json_pointer_destroy(suffix); +} + +/* ── Comparison ────────────────────────────────────────────────────────── */ + +TEST(PointerTest, Compare) { + celix_json_pointer_t* a = celix_json_pointer_create("/a/b"); + celix_json_pointer_t* b = celix_json_pointer_create("/a/b"); + celix_json_pointer_t* c = celix_json_pointer_create("/a/c"); + + EXPECT_TRUE(celix_json_pointer_equals(a, b)); + EXPECT_FALSE(celix_json_pointer_equals(a, c)); + EXPECT_FALSE(celix_json_pointer_equals(a, nullptr)); + + celix_json_pointer_destroy(a); + celix_json_pointer_destroy(b); + celix_json_pointer_destroy(c); +} + +/* ── Copy ──────────────────────────────────────────────────────────────── */ + +TEST(PointerTest, Copy) { + celix_json_pointer_t* orig = celix_json_pointer_create("/foo/bar"); + celix_json_pointer_t* copy = celix_json_pointer_copy(orig); + + EXPECT_EQ(celix_json_pointer_depth(orig), celix_json_pointer_depth(copy)); + EXPECT_TRUE(celix_json_pointer_equals(orig, copy)); + + /* Mutate copy — original should be unaffected */ + celix_json_pointer_pop(copy); + EXPECT_EQ(2u, celix_json_pointer_depth(orig)); + EXPECT_EQ(1u, celix_json_pointer_depth(copy)); + + celix_json_pointer_destroy(orig); + celix_json_pointer_destroy(copy); +} + +/* ── Stack allocation ──────────────────────────────────────────────────── */ + +TEST(PointerTest, StackAllocated) { + celix_json_pointer_t ptr; + + EXPECT_EQ(0, celix_json_pointer_init(&ptr, "/a/b")); + EXPECT_EQ(2u, celix_json_pointer_depth(&ptr)); + EXPECT_STREQ("a", celix_json_pointer_token(&ptr, 0)); + + celix_json_pointer_push(&ptr, "c"); + EXPECT_EQ(3u, celix_json_pointer_depth(&ptr)); + + celix_json_pointer_clear(&ptr); +} + +/* ── get-or-create ─────────────────────────────────────────────────────── */ + +TEST(PointerTest, GetOrCreate) { + json_t* doc = json_object(); + celix_json_pointer_t* p = celix_json_pointer_create("/x/y/z"); + ASSERT_NE(nullptr, p); + + json_t* node = celix_json_pointer_get_or_create(doc, p); + ASSERT_NE(nullptr, node); + json_decref(node); /* get-or-create returns a new reference */ + + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ("{\"x\":{\"y\":{\"z\":null}}}", s); + free(s); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * Extended tests — inspired by nlohmann/json's unit-json_pointer.cpp + * ═══════════════════════════════════════════════════════════════════════════ */ + +/* ── RFC 6901 §5 canonical fixture ───────────────────────────────────── */ + +TEST(PointerTest, Rfc6901Section5) { + /* The example document from RFC 6901 §5 */ + const char* json_str = R"({ + "foo": ["bar", "baz"], + "": 0, + "a/b": 1, + "c%d": 2, + "e^f": 3, + "g|h": 4, + "i\\j": 5, + "k\"l": 6, + " ": 7, + "m~n": 8 + })"; + json_t* doc = json_loads(json_str, JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + /* Test all keys from the RFC example */ + auto check = [&](const char* ptr_str, json_t* expected, bool owned) { + celix_json_pointer_t p; + ASSERT_EQ(0, celix_json_pointer_init(&p, ptr_str)) << "Failed: " << ptr_str; + json_t* v = celix_json_pointer_get(doc, &p); + ASSERT_NE(nullptr, v) << "Missing: " << ptr_str; + EXPECT_TRUE(json_equal(expected, v)) << "Mismatch at: " << ptr_str; + celix_json_pointer_clear(&p); + if (owned) + json_decref(expected); + }; + + check("/foo", json_object_get(doc, "foo"), false); + check("/foo/0", json_string("bar"), true); + check("/", json_integer(0), true); + check("/a~1b", json_integer(1), true); + check("/c%d", json_integer(2), true); + check("/e^f", json_integer(3), true); + check("/g|h", json_integer(4), true); + check("/i\\j", json_integer(5), true); + check("/k\"l", json_integer(6), true); + check("/ ", json_integer(7), true); + check("/m~0n", json_integer(8), true); + + json_decref(doc); +} + +/* ── Array index edge cases ──────────────────────────────────────────── */ + +TEST(PointerTest, LeadingZeroInvalid) { + celix_json_pointer_t p; + /* Leading zero in array index is invalid per RFC 6901 */ + EXPECT_NE(0, celix_json_pointer_init(&p, "/foo/01")); +} + +TEST(PointerTest, NonNumericArrayIndex) { + celix_json_pointer_t p; + /* Non-numeric token used as array index — pointer parses but resolution fails */ + json_t* doc = json_loads("[1,2,3]", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + ASSERT_EQ(0, celix_json_pointer_init(&p, "/one")); + EXPECT_EQ(nullptr, celix_json_pointer_get(doc, &p)); + + celix_json_pointer_clear(&p); + json_decref(doc); +} + +TEST(PointerTest, ArithmeticInIndex) { + json_t* doc = json_loads("[1,2,3]", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t p; + ASSERT_EQ(0, celix_json_pointer_init(&p, "/1+1")); + EXPECT_EQ(nullptr, celix_json_pointer_get(doc, &p)); + + celix_json_pointer_clear(&p); + json_decref(doc); +} + +/* ── Invalid escape sequences ────────────────────────────────────────── */ + +TEST(PointerTest, InvalidEscapeSequence) { + celix_json_pointer_t p; + /* "~~" — stray tilde not followed by valid escape */ + EXPECT_NE(0, celix_json_pointer_init(&p, "/foo/~~")); + celix_json_pointer_clear(&p); + + /* "~" at end — tilde with nothing after */ + EXPECT_NE(0, celix_json_pointer_init(&p, "/foo/~")); + celix_json_pointer_clear(&p); + + /* "~2" — invalid escape character */ + EXPECT_NE(0, celix_json_pointer_init(&p, "/foo/~2")); + celix_json_pointer_clear(&p); +} + +/* ── Single-token array index ────────────────────────────────────────── */ + +TEST(PointerTest, ArrayAccessByIndex) { + json_t* doc = json_loads("[10,20,30]", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t p; + ASSERT_EQ(0, celix_json_pointer_init(&p, "/0")); + json_t* v = celix_json_pointer_get(doc, &p); + ASSERT_NE(nullptr, v); + EXPECT_EQ(10, json_integer_value(v)); + celix_json_pointer_clear(&p); + + ASSERT_EQ(0, celix_json_pointer_init(&p, "/1")); + v = celix_json_pointer_get(doc, &p); + EXPECT_EQ(20, json_integer_value(v)); + celix_json_pointer_clear(&p); + + ASSERT_EQ(0, celix_json_pointer_init(&p, "/2")); + v = celix_json_pointer_get(doc, &p); + EXPECT_EQ(30, json_integer_value(v)); + celix_json_pointer_clear(&p); + + /* Out of bounds */ + ASSERT_EQ(0, celix_json_pointer_init(&p, "/3")); + EXPECT_EQ(nullptr, celix_json_pointer_get(doc, &p)); + celix_json_pointer_clear(&p); + + json_decref(doc); +} + +/* ── Nested array access ─────────────────────────────────────────────── */ + +TEST(PointerTest, NestedArrayAccess) { + json_t* doc = json_loads("[[1,2],[3,4]]", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t p; + ASSERT_EQ(0, celix_json_pointer_init(&p, "/0/1")); + json_t* v = celix_json_pointer_get(doc, &p); + ASSERT_NE(nullptr, v); + EXPECT_EQ(2, json_integer_value(v)); + celix_json_pointer_clear(&p); + + ASSERT_EQ(0, celix_json_pointer_init(&p, "/1/0")); + v = celix_json_pointer_get(doc, &p); + ASSERT_NE(nullptr, v); + EXPECT_EQ(3, json_integer_value(v)); + celix_json_pointer_clear(&p); + + json_decref(doc); +} + +/* ── Set on existing key overwrites ──────────────────────────────────── */ + +TEST(PointerTest, SetOverwritesExisting) { + json_t* doc = json_loads(R"({"a":1,"b":2})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t p; + ASSERT_EQ(0, celix_json_pointer_init(&p, "/b")); + + EXPECT_EQ(0, celix_json_pointer_set(doc, &p, json_integer(99))); + + /* Verify overwrite */ + json_t* v = celix_json_pointer_get(doc, &p); + ASSERT_NE(nullptr, v); + EXPECT_EQ(99, json_integer_value(v)); + + /* 'a' should still be 1 */ + celix_json_pointer_t pa; + ASSERT_EQ(0, celix_json_pointer_init(&pa, "/a")); + v = celix_json_pointer_get(doc, &pa); + EXPECT_EQ(1, json_integer_value(v)); + + celix_json_pointer_clear(&p); + celix_json_pointer_clear(&pa); + json_decref(doc); +} + +/* ── Set array with gap fills nulls ──────────────────────────────────── */ + +TEST(PointerTest, SetArrayWithGap) { + json_t* doc = json_loads(R"({"arr":[1,2]})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t p; + ASSERT_EQ(0, celix_json_pointer_init(&p, "/arr/4")); + + EXPECT_EQ(0, celix_json_pointer_set(doc, &p, json_integer(99))); + + /* Verify array was extended with nulls */ + celix_json_pointer_t pa; + ASSERT_EQ(0, celix_json_pointer_init(&pa, "/arr")); + json_t* arr = celix_json_pointer_get(doc, &pa); + ASSERT_NE(nullptr, arr); + EXPECT_EQ(5u, json_array_size(arr)); + EXPECT_EQ(99, json_integer_value(json_array_get(arr, 4))); + /* Positions 2, 3 should be null */ + EXPECT_TRUE(json_is_null(json_array_get(arr, 2))); + EXPECT_TRUE(json_is_null(json_array_get(arr, 3))); + + celix_json_pointer_clear(&p); + celix_json_pointer_clear(&pa); + json_decref(doc); +} + +/* ── Get on `/-` returns NULL ────────────────────────────────────────── */ + +TEST(PointerTest, DashIndexGetReturnsNull) { + json_t* doc = json_loads("[1,2,3]", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t p; + ASSERT_EQ(0, celix_json_pointer_init(&p, "/-")); + EXPECT_EQ(nullptr, celix_json_pointer_get(doc, &p)); + + celix_json_pointer_clear(&p); + json_decref(doc); +} + +/* ── Resolving into scalar returns NULL ──────────────────────────────── */ + +TEST(PointerTest, ResolveIntoScalar) { + json_t* doc = json_loads(R"({"a":42,"b":{"c":1}})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t p; + + /* "/a" is 42 (scalar) — can't descend further */ + ASSERT_EQ(0, celix_json_pointer_init(&p, "/a/foo")); + EXPECT_EQ(nullptr, celix_json_pointer_get(doc, &p)); + celix_json_pointer_clear(&p); + + /* "/a" is 42 (scalar) — get_or_create should also fail */ + ASSERT_EQ(0, celix_json_pointer_init(&p, "/a/foo")); + EXPECT_EQ(nullptr, celix_json_pointer_get_or_create(doc, &p)); + celix_json_pointer_clear(&p); + + json_decref(doc); +} + +/* ── Remove non-existent key ─────────────────────────────────────────── */ + +TEST(PointerTest, RemoveNonExistent) { + json_t* doc = json_loads(R"({"a":1})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t p; + ASSERT_EQ(0, celix_json_pointer_init(&p, "/x")); + EXPECT_NE(0, celix_json_pointer_remove(doc, &p)); /* should fail */ + + /* Document unchanged */ + json_t* v = json_object_get(doc, "a"); + ASSERT_NE(nullptr, v); + EXPECT_EQ(1, json_integer_value(v)); + + celix_json_pointer_clear(&p); + json_decref(doc); +} + +/* ── Remove root ─────────────────────────────────────────────────────── */ + +TEST(PointerTest, RemoveRootReturnsError) { + json_t* doc = json_loads("[1,2]", JSON_DECODE_ANY, nullptr); + celix_json_pointer_t p; + memset(&p, 0, sizeof(p)); /* empty → root */ + EXPECT_NE(0, celix_json_pointer_remove(doc, &p)); + celix_json_pointer_clear(&p); + json_decref(doc); +} + +/* ── Token operations: front/back ────────────────────────────────────── */ + +TEST(PointerTest, TokenFrontBack) { + celix_json_pointer_t p; + ASSERT_EQ(0, celix_json_pointer_init(&p, "/a/b/c")); + + /* token(0) is front, token(depth-1) is back */ + EXPECT_STREQ("a", celix_json_pointer_token(&p, 0)); + EXPECT_STREQ("c", celix_json_pointer_token(&p, celix_json_pointer_depth(&p) - 1)); + + celix_json_pointer_clear(&p); +} + +/* ── Pointer on non-object/array root ────────────────────────────────── */ + +TEST(PointerTest, PointerOnScalarRoot) { + json_t* doc = json_integer(42); + + /* Root pointer returns the scalar */ + celix_json_pointer_t p; + memset(&p, 0, sizeof(p)); + json_t* v = celix_json_pointer_get(doc, &p); + ASSERT_NE(nullptr, v); + EXPECT_EQ(42, json_integer_value(v)); + celix_json_pointer_clear(&p); + + /* Non-root pointer on scalar returns NULL */ + ASSERT_EQ(0, celix_json_pointer_init(&p, "/foo")); + EXPECT_EQ(nullptr, celix_json_pointer_get(doc, &p)); + celix_json_pointer_clear(&p); + + json_decref(doc); +} + +/* ── get_or_create with array intermediates ──────────────────────────── */ + +TEST(PointerTest, SetCreatesArrayIntermediate) { + /* set handles multi-level paths with array creation better */ + json_t* doc = json_object(); + celix_json_pointer_t p; + ASSERT_EQ(0, celix_json_pointer_init(&p, "/list/0/item")); + + EXPECT_EQ(0, celix_json_pointer_set(doc, &p, json_string("hello"))); + + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ(R"({"list":[{"item":"hello"}]})", s); + free(s); + + celix_json_pointer_clear(&p); + json_decref(doc); +} + +TEST(PointerTest, GetOrCreateSimpleArray) { + json_t* doc = json_object(); + celix_json_pointer_t p; + /* "/arr" creates an object, then push "2" — but get_or_create uses token heuristic: + * "arr" → object (non-numeric), "2" → object key in that object. + * For true array creation, use set which has the "look ahead" heuristic. */ + json_t* arr = json_array(); + json_array_append_new(arr, json_integer(10)); + json_array_append_new(arr, json_integer(20)); + json_object_set_new(doc, "arr", arr); + + /* Now "/arr/1" → get element at index 1 */ + ASSERT_EQ(0, celix_json_pointer_init(&p, "/arr/1")); + json_t* node = celix_json_pointer_get_or_create(doc, &p); + ASSERT_NE(nullptr, node); + EXPECT_EQ(20, json_integer_value(node)); + json_decref(node); + + /* "/arr/2" → extend and return new null */ + celix_json_pointer_clear(&p); + ASSERT_EQ(0, celix_json_pointer_init(&p, "/arr/3")); + node = celix_json_pointer_get_or_create(doc, &p); + ASSERT_NE(nullptr, node); + EXPECT_TRUE(json_is_null(node)); + json_decref(node); + + EXPECT_EQ(4u, json_array_size(json_object_get(doc, "arr"))); + celix_json_pointer_clear(&p); + json_decref(doc); +} + +/* ── get_or_create on existing path ──────────────────────────────────── */ + +TEST(PointerTest, GetOrCreateExisting) { + json_t* doc = json_loads(R"({"a":{"b":42}})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t p; + ASSERT_EQ(0, celix_json_pointer_init(&p, "/a/b")); + + json_t* v = celix_json_pointer_get_or_create(doc, &p); + ASSERT_NE(nullptr, v); + EXPECT_EQ(42, json_integer_value(v)); + json_decref(v); /* get_or_create returns a new reference */ + + /* Document unchanged */ + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ(R"({"a":{"b":42}})", s); + free(s); + + celix_json_pointer_clear(&p); + json_decref(doc); +} + +/* ── Escape edge: ~01 should be ~1 then 1, not ~0 + 1 ────────────────── */ + +TEST(PointerTest, EscapeOrderMatters) { + /* "~01" in a pointer string means: unescape ~0→~, then the '1' is just '1' + * So "/m~01n" should resolve to the key "m~1n", NOT "m~0" + "1n" or anything else. */ + celix_json_pointer_t p; + ASSERT_EQ(0, celix_json_pointer_init(&p, "/m~01n")); + EXPECT_EQ(1u, celix_json_pointer_depth(&p)); + /* ~0 → ~, then 1 remains as-is */ + EXPECT_STREQ("m~1n", celix_json_pointer_token(&p, 0)); + celix_json_pointer_clear(&p); +} + +/* ── Escaped path resolution on a real document ──────────────────────── */ + +TEST(PointerTest, EscapedPathResolution) { + json_t* doc = json_loads(R"({"a/b":42,"m~n":99})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + /* "/a~1b" should resolve to key "a/b" */ + celix_json_pointer_t p; + ASSERT_EQ(0, celix_json_pointer_init(&p, "/a~1b")); + json_t* v = celix_json_pointer_get(doc, &p); + ASSERT_NE(nullptr, v); + EXPECT_EQ(42, json_integer_value(v)); + celix_json_pointer_clear(&p); + + /* "/m~0n" should resolve to key "m~n" */ + ASSERT_EQ(0, celix_json_pointer_init(&p, "/m~0n")); + v = celix_json_pointer_get(doc, &p); + ASSERT_NE(nullptr, v); + EXPECT_EQ(99, json_integer_value(v)); + celix_json_pointer_clear(&p); + + json_decref(doc); +} + +/* ── Empty token (trailing slash or double slash) ────────────────────── */ + +TEST(PointerTest, EmptyToken) { + /* "" is a valid JSON key — "/foo/" has two tokens: "foo" and "" */ + json_t* doc = json_loads(R"({"foo":{"":99}})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t p; + ASSERT_EQ(0, celix_json_pointer_init(&p, "/foo/")); + EXPECT_EQ(2u, celix_json_pointer_depth(&p)); + EXPECT_STREQ("foo", celix_json_pointer_token(&p, 0)); + EXPECT_STREQ("", celix_json_pointer_token(&p, 1)); + + json_t* v = celix_json_pointer_get(doc, &p); + ASSERT_NE(nullptr, v); + EXPECT_EQ(99, json_integer_value(v)); + + celix_json_pointer_clear(&p); + json_decref(doc); +} + +/* ── Percent-encoded characters in pointer ───────────────────────────── */ + +TEST(PointerTest, PercentEncodingIsLiteral) { + /* RFC 6901: %XX is NOT decoded; it's just the literal characters */ + json_t* doc = json_loads(R"({"%25": "percent"})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t p; + ASSERT_EQ(0, celix_json_pointer_init(&p, "/%25")); + json_t* v = celix_json_pointer_get(doc, &p); + ASSERT_NE(nullptr, v); + EXPECT_STREQ("percent", json_string_value(v)); + + celix_json_pointer_clear(&p); + json_decref(doc); +} + +/* ── Set with new nesting creates objects by default ─────────────────── */ + +TEST(PointerTest, SetCreatesIntermediateObjects) { + json_t* doc = json_object(); + celix_json_pointer_t p; + ASSERT_EQ(0, celix_json_pointer_init(&p, "/config/database/host")); + + EXPECT_EQ(0, celix_json_pointer_set(doc, &p, json_string("localhost"))); + + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ(R"({"config":{"database":{"host":"localhost"}}})", s); + free(s); + + celix_json_pointer_clear(&p); + json_decref(doc); +} + +/* ── Comparison: same tokens, different order ────────────────────────── */ + +TEST(PointerTest, CompareDifferentOrder) { + celix_json_pointer_t a, b; + celix_json_pointer_init(&a, "/a/b"); + celix_json_pointer_init(&b, "/b/a"); + EXPECT_FALSE(celix_json_pointer_equals(&a, &b)); + celix_json_pointer_clear(&a); + celix_json_pointer_clear(&b); +} + +TEST(PointerTest, CompareDifferentLength) { + celix_json_pointer_t a, b; + celix_json_pointer_init(&a, "/a/b"); + celix_json_pointer_init(&b, "/a/b/c"); + EXPECT_FALSE(celix_json_pointer_equals(&a, &b)); + celix_json_pointer_clear(&a); + celix_json_pointer_clear(&b); +} + +/* ── Push with special characters gets escaped in to_string ──────────── */ + +TEST(PointerTest, PushWithSlashReEncoded) { + celix_json_pointer_t p; + memset(&p, 0, sizeof(p)); + celix_json_pointer_push(&p, "a/b"); + celix_json_pointer_push(&p, "c~d"); + + char* s = celix_json_pointer_to_string(&p); + EXPECT_STREQ("/a~1b/c~0d", s); + free(s); + + celix_json_pointer_clear(&p); +} + +/* ── Set new via new API ─────────────────────────────────────────────── */ + +TEST(PointerTest, SetNewIncrementsRef) { + json_t* doc = json_object(); + celix_json_pointer_t p; + ASSERT_EQ(0, celix_json_pointer_init(&p, "/x")); + + json_t* val = json_string("hello"); + EXPECT_EQ(0, celix_json_pointer_set_new(doc, &p, val)); + + /* val should still be valid (set_new incref'd it) */ + EXPECT_STREQ("hello", json_string_value(val)); + + json_t* v = celix_json_pointer_get(doc, &p); + ASSERT_NE(nullptr, v); + EXPECT_TRUE(json_equal(val, v)); + + json_decref(val); + celix_json_pointer_clear(&p); + json_decref(doc); +} + +/* ── Stack-allocated init/clear repeated ─────────────────────────────── */ + +TEST(PointerTest, StackReuse) { + celix_json_pointer_t p; + + ASSERT_EQ(0, celix_json_pointer_init(&p, "/a/b")); + EXPECT_EQ(2u, celix_json_pointer_depth(&p)); + celix_json_pointer_clear(&p); + + /* Re-init with different path */ + ASSERT_EQ(0, celix_json_pointer_init(&p, "/x/y/z")); + EXPECT_EQ(3u, celix_json_pointer_depth(&p)); + EXPECT_STREQ("x", celix_json_pointer_token(&p, 0)); + EXPECT_STREQ("y", celix_json_pointer_token(&p, 1)); + EXPECT_STREQ("z", celix_json_pointer_token(&p, 2)); + celix_json_pointer_clear(&p); +} + +/* ── celix_json_pointer_contains ───────────────────────────────────────── */ + +TEST(PointerTest, Contains) { + json_t* doc = json_loads(R"({"foo":{"bar":42},"arr":[1,2,3]})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t p; + memset(&p, 0, sizeof(p)); + + /* Root pointer always exists */ + EXPECT_EQ(1, celix_json_pointer_contains(doc, &p)); + + /* Existing paths */ + ASSERT_EQ(0, celix_json_pointer_init(&p, "/foo")); + EXPECT_EQ(1, celix_json_pointer_contains(doc, &p)); + celix_json_pointer_clear(&p); + + ASSERT_EQ(0, celix_json_pointer_init(&p, "/foo/bar")); + EXPECT_EQ(1, celix_json_pointer_contains(doc, &p)); + celix_json_pointer_clear(&p); + + ASSERT_EQ(0, celix_json_pointer_init(&p, "/arr")); + EXPECT_EQ(1, celix_json_pointer_contains(doc, &p)); + celix_json_pointer_clear(&p); + + ASSERT_EQ(0, celix_json_pointer_init(&p, "/arr/1")); + EXPECT_EQ(1, celix_json_pointer_contains(doc, &p)); + celix_json_pointer_clear(&p); + + /* Non-existing paths */ + ASSERT_EQ(0, celix_json_pointer_init(&p, "/arr/3")); + EXPECT_EQ(0, celix_json_pointer_contains(doc, &p)); + celix_json_pointer_clear(&p); + + ASSERT_EQ(0, celix_json_pointer_init(&p, "/foo/baz")); + EXPECT_EQ(0, celix_json_pointer_contains(doc, &p)); + celix_json_pointer_clear(&p); + + ASSERT_EQ(0, celix_json_pointer_init(&p, "/missing")); + EXPECT_EQ(0, celix_json_pointer_contains(doc, &p)); + celix_json_pointer_clear(&p); + + /* "-" token returns 0 (no such array element) */ + ASSERT_EQ(0, celix_json_pointer_init(&p, "/arr/-")); + EXPECT_EQ(0, celix_json_pointer_contains(doc, &p)); + celix_json_pointer_clear(&p); + + json_decref(doc); +} + +TEST(PointerTest, ContainsNullDoc) { + celix_json_pointer_t p; + ASSERT_EQ(0, celix_json_pointer_init(&p, "/x")); + + /* NULL doc is safe — treated as not containing */ + EXPECT_EQ(0, celix_json_pointer_contains(nullptr, &p)); + + celix_json_pointer_clear(&p); +} + +TEST(PointerTest, ContainsScalarDoc) { + json_t* doc = json_integer(42); + + celix_json_pointer_t p; + memset(&p, 0, sizeof(p)); + + /* Root pointer on scalar returns 1 */ + EXPECT_EQ(1, celix_json_pointer_contains(doc, &p)); + + /* Non-root on scalar returns 0 */ + ASSERT_EQ(0, celix_json_pointer_init(&p, "/x")); + EXPECT_EQ(0, celix_json_pointer_contains(doc, &p)); + celix_json_pointer_clear(&p); + + json_decref(doc); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * Coverage edge cases — NULL guards, RFC 6901 validation error paths + * ═══════════════════════════════════════════════════════════════════════════ */ + +/* ── Lifecycle and NULL guards ─────────────────────────────────────── */ + +TEST(PointerTest, InitNull) { + EXPECT_EQ(-1, celix_json_pointer_init(nullptr, "/a")); +} + +TEST(PointerTest, InitLeadingZeroNonNumeric) { + /* Leading-zero token containing a non-digit is NOT an RFC 6901 array index — + * it is a plain object key, so init must succeed. */ + celix_json_pointer_t p; + ASSERT_EQ(0, celix_json_pointer_init(&p, "/0a")); + EXPECT_EQ(1u, celix_json_pointer_depth(&p)); + EXPECT_STREQ("0a", celix_json_pointer_token(&p, 0)); + celix_json_pointer_clear(&p); + + ASSERT_EQ(0, celix_json_pointer_init(&p, "/01a")); + EXPECT_EQ(1u, celix_json_pointer_depth(&p)); + EXPECT_STREQ("01a", celix_json_pointer_token(&p, 0)); + celix_json_pointer_clear(&p); +} + +TEST(PointerTest, CopyNull) { + EXPECT_EQ(nullptr, celix_json_pointer_copy(nullptr)); +} + +TEST(PointerTest, DestroyNull) { + celix_json_pointer_destroy(nullptr); /* must be a safe no-op */ +} + +TEST(PointerTest, ClearNull) { + celix_json_pointer_clear(nullptr); /* must be a safe no-op */ +} + +TEST(PointerTest, PushNull) { + celix_json_pointer_t p; + memset(&p, 0, sizeof(p)); + + EXPECT_EQ(-1, celix_json_pointer_push(nullptr, "a")); + EXPECT_EQ(-1, celix_json_pointer_push(&p, nullptr)); +} + +TEST(PointerTest, ToStringNull) { + EXPECT_EQ(nullptr, celix_json_pointer_to_string(nullptr)); +} + +TEST(PointerTest, EscapeNull) { + EXPECT_EQ(nullptr, celix_json_pointer_escape(nullptr)); +} + +TEST(PointerTest, UnescapeNull) { + EXPECT_EQ(nullptr, celix_json_pointer_unescape(nullptr)); +} + +TEST(PointerTest, ParentNullAndRoot) { + EXPECT_EQ(nullptr, celix_json_pointer_parent(nullptr, nullptr)); + + /* Parent of the root (empty pointer) has no parent */ + celix_json_pointer_t* p = celix_json_pointer_create(""); + ASSERT_NE(nullptr, p); + EXPECT_EQ(nullptr, celix_json_pointer_parent(p, nullptr)); + celix_json_pointer_destroy(p); +} + +TEST(PointerTest, ConcatNull) { + celix_json_pointer_t* p = celix_json_pointer_create("/a"); + celix_json_pointer_t* suffix = celix_json_pointer_create("/b"); + ASSERT_NE(nullptr, p); + ASSERT_NE(nullptr, suffix); + + EXPECT_EQ(-1, celix_json_pointer_concat(nullptr, suffix)); + EXPECT_EQ(-1, celix_json_pointer_concat(p, nullptr)); + + celix_json_pointer_destroy(p); + celix_json_pointer_destroy(suffix); +} + +TEST(PointerTest, EqualsSelf) { + celix_json_pointer_t* p = celix_json_pointer_create("/a/b"); + ASSERT_NE(nullptr, p); + EXPECT_TRUE(celix_json_pointer_equals(p, p)); /* same pointer short-circuit */ + celix_json_pointer_destroy(p); +} + +/* ── get_or_create branches ────────────────────────────────────────── */ + +TEST(PointerTest, GetOrCreateNullDoc) { + celix_json_pointer_t* p = celix_json_pointer_create("/a"); + ASSERT_NE(nullptr, p); + + EXPECT_EQ(nullptr, celix_json_pointer_get_or_create(nullptr, p)); + + /* Scalar root is not a container */ + json_t* doc = json_integer(42); + EXPECT_EQ(nullptr, celix_json_pointer_get_or_create(doc, p)); + + json_decref(doc); + celix_json_pointer_destroy(p); +} + +TEST(PointerTest, GetOrCreateEmptyTokenObject) { + json_t* doc = json_object(); + celix_json_pointer_t* p = celix_json_pointer_create("/a//b"); + ASSERT_NE(nullptr, p); + + json_t* node = celix_json_pointer_get_or_create(doc, p); + ASSERT_NE(nullptr, node); + EXPECT_TRUE(json_is_null(node)); + json_decref(node); + + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ("{\"a\":{\"\":{\"b\":null}}}", s); + free(s); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, GetOrCreateLeadingZeroObject) { + json_t* doc = json_object(); + /* "01" cannot be parsed via init (RFC 6901 leading-zero rule), so build + * the pointer with push to exercise the leading-zero heuristic. */ + celix_json_pointer_t* p = celix_json_pointer_create(""); + ASSERT_NE(nullptr, p); + ASSERT_EQ(0, celix_json_pointer_push(p, "a")); + ASSERT_EQ(0, celix_json_pointer_push(p, "01")); + ASSERT_EQ(0, celix_json_pointer_push(p, "b")); + + json_t* node = celix_json_pointer_get_or_create(doc, p); + ASSERT_NE(nullptr, node); + json_decref(node); + + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ("{\"a\":{\"01\":{\"b\":null}}}", s); + free(s); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, GetOrCreateDashAppend) { + json_t* doc = json_loads("[1,2,3]", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t* p = celix_json_pointer_create("/-"); + ASSERT_NE(nullptr, p); + + /* "-" as the last token appends a null element and returns the array (new reference) */ + json_t* node = celix_json_pointer_get_or_create(doc, p); + ASSERT_NE(nullptr, node); + EXPECT_TRUE(json_equal(node, doc)); + EXPECT_EQ(4u, json_array_size(node)); + EXPECT_TRUE(json_is_null(json_array_get(node, 3))); + json_decref(node); + + /* Document has the appended null element */ + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ("[1,2,3,null]", s); + free(s); + + /* "-" can never resolve to an existing element: a second call appends again */ + json_t* node2 = celix_json_pointer_get_or_create(doc, p); + ASSERT_NE(nullptr, node2); + EXPECT_EQ(5u, json_array_size(node2)); + json_decref(node2); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, GetOrCreateDashIntermediate) { + json_t* doc = json_loads("[[1,2]]", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t* p = celix_json_pointer_create("/-/0"); + ASSERT_NE(nullptr, p); + + /* "-" not in last position is invalid */ + EXPECT_EQ(nullptr, celix_json_pointer_get_or_create(doc, p)); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, GetOrCreateNonNumericArrayIndex) { + json_t* doc = json_loads("[1,2]", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t* p = celix_json_pointer_create("/abc"); + ASSERT_NE(nullptr, p); + + EXPECT_EQ(nullptr, celix_json_pointer_get_or_create(doc, p)); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +/* ── set branches ──────────────────────────────────────────────────── */ + +TEST(PointerTest, SetNullArgs) { + json_t* doc = json_object(); + celix_json_pointer_t* p = celix_json_pointer_create("/a"); + ASSERT_NE(nullptr, p); + celix_json_pointer_t* empty = celix_json_pointer_create(""); + ASSERT_NE(nullptr, empty); + + /* NULL guards return before consuming value — caller keeps ownership */ + json_t* v1 = json_integer(1); + EXPECT_EQ(-1, celix_json_pointer_set(nullptr, p, v1)); + json_decref(v1); + + json_t* v2 = json_integer(2); + EXPECT_EQ(-1, celix_json_pointer_set(doc, nullptr, v2)); + json_decref(v2); + + json_t* v3 = json_integer(3); + EXPECT_EQ(-1, celix_json_pointer_set(doc, empty, v3)); + json_decref(v3); + + celix_json_pointer_destroy(p); + celix_json_pointer_destroy(empty); + json_decref(doc); +} + +TEST(PointerTest, SetNextTokenEmptyObject) { + json_t* doc = json_object(); + celix_json_pointer_t* p = celix_json_pointer_create("/a/"); + ASSERT_NE(nullptr, p); + + EXPECT_EQ(0, celix_json_pointer_set(doc, p, json_integer(7))); + + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ("{\"a\":{\"\":7}}", s); + free(s); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, SetNextTokenLeadingZeroObject) { + json_t* doc = json_object(); + /* "01" cannot be parsed via init (RFC 6901 leading-zero rule), so build + * the pointer with push to exercise the leading-zero heuristic. */ + celix_json_pointer_t* p = celix_json_pointer_create(""); + ASSERT_NE(nullptr, p); + ASSERT_EQ(0, celix_json_pointer_push(p, "a")); + ASSERT_EQ(0, celix_json_pointer_push(p, "01")); + ASSERT_EQ(0, celix_json_pointer_push(p, "b")); + + EXPECT_EQ(0, celix_json_pointer_set(doc, p, json_integer(7))); + + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ("{\"a\":{\"01\":{\"b\":7}}}", s); + free(s); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, SetNonNumericArrayIntermediate) { + json_t* doc = json_loads("[1,2]", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t* p = celix_json_pointer_create("/abc/0"); + ASSERT_NE(nullptr, p); + + /* Fails and consumes (decrefs) the value */ + EXPECT_EQ(-1, celix_json_pointer_set(doc, p, json_integer(9))); + + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ("[1,2]", s); + free(s); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, SetReplacePrimitiveWithObjectEmpty) { + json_t* doc = json_loads("[1]", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t* p = celix_json_pointer_create("/0/"); + ASSERT_NE(nullptr, p); + + EXPECT_EQ(0, celix_json_pointer_set(doc, p, json_integer(9))); + + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ("[{\"\":9}]", s); + free(s); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, SetReplacePrimitiveWithObjectLeadingZero) { + json_t* doc = json_loads("[1]", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + /* "01" cannot be parsed via init (RFC 6901 leading-zero rule), so build + * the pointer with push to exercise the leading-zero heuristic. */ + celix_json_pointer_t* p = celix_json_pointer_create(""); + ASSERT_NE(nullptr, p); + ASSERT_EQ(0, celix_json_pointer_push(p, "0")); + ASSERT_EQ(0, celix_json_pointer_push(p, "01")); + ASSERT_EQ(0, celix_json_pointer_push(p, "a")); + + EXPECT_EQ(0, celix_json_pointer_set(doc, p, json_integer(9))); + + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ("[{\"01\":{\"a\":9}}]", s); + free(s); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, SetScalarRootFails) { + json_t* doc = json_integer(1); + + celix_json_pointer_t* p = celix_json_pointer_create("/a/b"); + ASSERT_NE(nullptr, p); + + /* Intermediate is a scalar — fails and consumes the value */ + EXPECT_EQ(-1, celix_json_pointer_set(doc, p, json_integer(9))); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, SetNonNumericArrayLast) { + json_t* doc = json_loads("[1,2]", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t* p = celix_json_pointer_create("/abc"); + ASSERT_NE(nullptr, p); + + EXPECT_EQ(-1, celix_json_pointer_set(doc, p, json_integer(9))); + + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ("[1,2]", s); + free(s); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, SetScalarLastFails) { + json_t* doc = json_integer(1); + + celix_json_pointer_t* p = celix_json_pointer_create("/a"); + ASSERT_NE(nullptr, p); + + EXPECT_EQ(-1, celix_json_pointer_set(doc, p, json_integer(9))); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +/* ── remove branches ───────────────────────────────────────────────── */ + +TEST(PointerTest, RemoveMissingParent) { + json_t* doc = json_object(); + + celix_json_pointer_t* p = celix_json_pointer_create("/a/b"); + ASSERT_NE(nullptr, p); + + EXPECT_EQ(-1, celix_json_pointer_remove(doc, p)); + + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ("{}", s); + free(s); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, RemoveNonNumericArrayIndex) { + json_t* doc = json_loads("[[1]]", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t* p = celix_json_pointer_create("/0/abc"); + ASSERT_NE(nullptr, p); + + EXPECT_EQ(-1, celix_json_pointer_remove(doc, p)); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, RemoveArrayIndexOutOfBounds) { + json_t* doc = json_loads("[1,2]", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, doc); + + celix_json_pointer_t* p = celix_json_pointer_create("/5"); + ASSERT_NE(nullptr, p); + + EXPECT_EQ(-1, celix_json_pointer_remove(doc, p)); + + char* s = json_dumps(doc, JSON_COMPACT); + EXPECT_STREQ("[1,2]", s); + free(s); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +TEST(PointerTest, RemoveScalarParent) { + json_t* doc = json_integer(1); + + celix_json_pointer_t* p = celix_json_pointer_create("/a"); + ASSERT_NE(nullptr, p); + + EXPECT_EQ(-1, celix_json_pointer_remove(doc, p)); + + celix_json_pointer_destroy(p); + json_decref(doc); +} + +/* ── Escape / navigation ───────────────────────────────────────────── */ + +TEST(PointerTest, UnescapeInvalidEscape) { + /* Invalid escapes are kept as-is: '~' is output and the next + * character is processed as a plain character. */ + char* out = celix_json_pointer_unescape("a~2b"); + ASSERT_NE(nullptr, out); + EXPECT_STREQ("a~2b", out); + free(out); + + out = celix_json_pointer_unescape("~"); + ASSERT_NE(nullptr, out); + EXPECT_STREQ("~", out); + free(out); +} + +TEST(PointerTest, ParentAllocated) { + celix_json_pointer_t* p = celix_json_pointer_create("/a/b"); + ASSERT_NE(nullptr, p); + + /* out == NULL → heap-allocated result (must be destroyed) */ + celix_json_pointer_t* pp = celix_json_pointer_parent(p, nullptr); + ASSERT_NE(nullptr, pp); + EXPECT_EQ(1u, celix_json_pointer_depth(pp)); + EXPECT_STREQ("a", celix_json_pointer_token(pp, 0)); + celix_json_pointer_destroy(pp); + + celix_json_pointer_destroy(p); +} + +/* ── Capacity growth ───────────────────────────────────────────────── */ + +TEST(PointerTest, CopyLongPointer) { + /* 9 tokens exceed the initial capacity of 8 → triggers the + * doubling growth loop in ensure_cap. */ + celix_json_pointer_t* p = celix_json_pointer_create("/a/b/c/d/e/f/g/h/i"); + ASSERT_NE(nullptr, p); + EXPECT_EQ(9u, celix_json_pointer_depth(p)); + + celix_json_pointer_t* copy = celix_json_pointer_copy(p); + ASSERT_NE(nullptr, copy); + EXPECT_EQ(9u, celix_json_pointer_depth(copy)); + EXPECT_TRUE(celix_json_pointer_equals(p, copy)); + + celix_json_pointer_destroy(p); + celix_json_pointer_destroy(copy); +} + +/* ── Automatic cleanup (celix_auto / celix_autoptr) ──────────────────────── */ + +TEST(PointerTest, AutoptrCreate) { + /* celix_autoptr → celix_json_pointer_destroy() runs at scope exit. */ + celix_autoptr(celix_json_pointer_t) p = celix_json_pointer_create("/a/b"); + ASSERT_NE(nullptr, p); + EXPECT_EQ(2u, celix_json_pointer_depth(p)); + EXPECT_STREQ("a", celix_json_pointer_token(p, 0)); + EXPECT_STREQ("b", celix_json_pointer_token(p, 1)); + + celix_autoptr(celix_json_pointer_t) root = celix_json_pointer_create(""); + ASSERT_NE(nullptr, root); + EXPECT_EQ(0u, celix_json_pointer_depth(root)); +} + +TEST(PointerTest, AutoptrNullIsSafe) { + /* "/a~" is an invalid escape → create fails → NULL. + * The cleanup function must skip NULL values without crashing. */ + celix_autoptr(celix_json_pointer_t) p = celix_json_pointer_create("/a~"); + EXPECT_EQ(nullptr, p); +} + +TEST(PointerTest, AutoInit) { + /* celix_auto → celix_json_pointer_clear() runs at scope exit. */ + celix_auto(celix_json_pointer_t) p; + ASSERT_EQ(0, celix_json_pointer_init(&p, "/a/b")); + EXPECT_EQ(2u, celix_json_pointer_depth(&p)); + EXPECT_STREQ("b", celix_json_pointer_token(&p, 1)); + + ASSERT_EQ(0, celix_json_pointer_push(&p, "c")); + EXPECT_EQ(3u, celix_json_pointer_depth(&p)); + celix_json_pointer_pop(&p); + EXPECT_EQ(2u, celix_json_pointer_depth(&p)); +} + +TEST(PointerTest, AutoInitFailureIsSafe) { + /* Failed init leaves the struct cleared; scope exit calls clear() + * again on the zeroed struct — must be a no-op. */ + celix_auto(celix_json_pointer_t) p; + EXPECT_EQ(-1, celix_json_pointer_init(&p, "/a~")); + EXPECT_EQ(0u, celix_json_pointer_depth(&p)); +} + +TEST(PointerTest, AutoZeroedStructSafe) { + /* clear() on a fully zeroed struct must be safe — the property + * celix_auto relies on for its scope-exit cleanup. */ + celix_auto(celix_json_pointer_t) p{}; + EXPECT_EQ(nullptr, p.tokens); + EXPECT_EQ(0u, p.len); +} diff --git a/libs/jansson_ext/gtest/src/test_ref.cpp b/libs/jansson_ext/gtest/src/test_ref.cpp new file mode 100644 index 000000000..d3c279f8d --- /dev/null +++ b/libs/jansson_ext/gtest/src/test_ref.cpp @@ -0,0 +1,772 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include + +#include "celix_json_patch.h" +#include "test_common.h" + +/* ── $ref to definitions ──────────────────────────────────────────────── */ + +TEST(RefTest, SimpleInternalRef) { + static const char* schema = R"({ + "definitions": { + "positiveInteger": { + "type": "integer", + "minimum": 1 + } + }, + "$ref": "#/definitions/positiveInteger" + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Valid: 5 is a positive integer */ + reset_errors(); + json_t* inst = json_loads("5", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + EXPECT_EQ(0, celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Invalid: 0 is not >= 1 */ + inst = json_loads("0", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr), 0); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── Unresolved $ref ───────────────────────────────────────────────────── */ + +TEST(RefTest, UnresolvedRef) { + static const char* schema = R"({ + "$ref": "#/definitions/nonexistent" + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + /* Should fail because the reference doesn't resolve */ + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, rc) + << "Expected failure for unresolved $ref, got: " << (errmsg ? errmsg : "success"); + free(errmsg); + + json_decref(sch); + free_validator(v); +} + +/* ── $ref to root ──────────────────────────────────────────────────────── */ + +TEST(RefTest, RefToRoot) { + static const char* schema = R"({ + "definitions": { + "posInt": { "type": "integer", "minimum": 1 } + }, + "type": "object", + "properties": { + "value": { "$ref": "#/definitions/posInt" } + }, + "required": ["value"] + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Valid: positive integer */ + reset_errors(); + json_t* inst = json_loads(R"({"value":99})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + EXPECT_EQ(0, celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Invalid: 0 is not >= 1 */ + inst = json_loads(R"({"value":0})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr), 0); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── $ref via JSON pointer ────────────────────────────────────────────── */ + +TEST(RefTest, RefViaJsonPointer) { + static const char* schema = R"({ + "properties": { + "name": { "$ref": "#/definitions/nameType" } + }, + "required": ["name"], + "definitions": { + "nameType": { "type": "string", "minLength": 1 } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Valid */ + reset_errors(); + json_t* inst = json_loads(R"({"name":"Alice"})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + EXPECT_EQ(0, celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Invalid: empty name */ + inst = json_loads(R"({"name":""})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr), 0); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── $ref with default override ────────────────────────────────────────── */ + +TEST(RefTest, RefWithDefaultOverride) { + static const char* schema = R"({ + "definitions": { + "withDefault": { + "type": "integer", + "default": 42 + } + }, + "properties": { + "val": { + "$ref": "#/definitions/withDefault", + "default": 99 + } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Validate with empty object — default should be generated */ + reset_errors(); + json_t* patch = nullptr; + json_t* inst = json_loads("{}", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + int n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, &patch); + EXPECT_EQ(0, n); + + /* Check that the patch includes the default */ + ASSERT_NE(nullptr, patch); + EXPECT_TRUE(json_is_array(patch)); + + json_decref(inst); + json_decref(patch); + json_decref(sch); + free_validator(v); +} + +/* ── $ref default passthrough (ref has no default, target has one) ──────── */ + +TEST(RefTest, RefDefaultValuePassthrough) { + static const char* schema = R"({ + "definitions": { + "withDefault": { + "type": "integer", + "default": 42 + } + }, + "type": "object", + "properties": { + "val": { + "$ref": "#/definitions/withDefault" + } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Validate empty object — the ref node carries no default of its own, so + * dv_ref must inherit the default from the referenced definition. */ + reset_errors(); + json_t* patch = nullptr; + json_t* inst = json_loads("{}", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + int n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, &patch); + EXPECT_EQ(0, n); + + json_t* filled = celix_json_patch_apply(inst, patch); + ASSERT_NE(nullptr, filled); + json_t* expected = json_loads(R"({"val":42})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, expected); + EXPECT_TRUE(json_equal(filled, expected)); + + json_decref(inst); + json_decref(patch); + json_decref(filled); + json_decref(expected); + json_decref(sch); + free_validator(v); +} + +/* ── Forward $ref to $id-based target (triggers root_insert placeholder) ─── */ + +TEST(RefTest, ForwardRefToIdBasedTarget) { + /* Definition "A" references "http://example.com/b" via $ref. + * Definition "B" declares $id "http://example.com/b" and appears + * after "A" in JSON key order. Since definitions are compiled + * iteratively, "A" sees a placeholder for "b"'s URI, which is + * resolved when "B" is registered via celix_jansson_schema_root_insert. + * This exercises the waiting-placeholder resolution at line 2167-2173. */ + static const char* schema = R"({ + "definitions": { + "A": { + "$ref": "http://example.com/b" + }, + "B": { + "$id": "http://example.com/b", + "type": "integer", + "minimum": 1 + } + }, + "properties": { + "value": { "$ref": "#/definitions/A" } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Valid: 5 resolves through A → B chain to positive integer */ + reset_errors(); + json_t* inst = json_loads(R"({"value":5})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + EXPECT_EQ(0, celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Invalid: 0 is not >= 1 */ + inst = json_loads(R"({"value":0})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr), 0); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── Forward $ref resolution (placeholder → registered schema) ───────────── */ + +TEST(RefTest, ForwardRefToDefinition) { + /* Forward reference: $ref to a definition that appears later in the + * JSON object. This exercises the placeholder → resolve path in + * celix_jansson_schema_root_insert, where a waiting placeholder is + * removed from sf->unresolved after the target node is registered. */ + static const char* schema = R"({ + "properties": { + "value": { "$ref": "#/definitions/posInt" } + }, + "definitions": { + "posInt": { + "type": "integer", + "minimum": 1 + } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Valid: positive integer */ + reset_errors(); + json_t* inst = json_loads(R"({"value":42})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + EXPECT_EQ(0, celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Invalid: 0 is not >= 1 */ + inst = json_loads(R"({"value":0})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr), 0); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── Multiple forward $refs to same definition ───────────────────────────── */ + +TEST(RefTest, MultipleForwardRefsToSameDefinition) { + /* Multiple forward $refs to the same definition. Each creates a + * placeholder in sf->unresolved; the first one that gets resolved + * via celix_jansson_schema_root_insert should remove and wire up + * the placeholder. All of them should be cleaned up without leak. */ + static const char* schema = R"({ + "properties": { + "a": { "$ref": "#/definitions/posInt" }, + "b": { "$ref": "#/definitions/posInt" } + }, + "definitions": { + "posInt": { + "type": "integer", + "minimum": 1 + } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Both properties should validate */ + reset_errors(); + json_t* inst = json_loads(R"({"a":5,"b":10})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + EXPECT_EQ(0, celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Both invalid */ + inst = json_loads(R"({"a":0,"b":-1})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(2, celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr)); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── External $ref with no loader ─────────────────────────────────────── */ + +TEST(RefTest, ExternalRefWithoutLoader) { + auto* v = celix_jansson_schema_validator_create( + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(R"({"$ref":"http://example.com/schema"})", 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_LOADER, rc); + free(errmsg); + + json_decref(sch); + free_validator(v); +} + +/* ── Loader returns error ──────────────────────────────────────────────── */ + +static int failing_loader(const char* /*uri*/, json_t** /*out*/, void* /*ud*/) { + return CELIX_JANSSON_SCHEMA_ERROR_LOADER; +} + +TEST(RefTest, LoaderReturnsError) { + auto* v = celix_jansson_schema_validator_create( + failing_loader, nullptr, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(R"({"$ref":"http://example.com/schema"})", 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_LOADER, rc); + free(errmsg); + + json_decref(sch); + free_validator(v); +} + +/* ── Loader returns non-schema value ───────────────────────────────────── */ + +static int non_schema_loader(const char* /*uri*/, json_t** out, void* /*ud*/) { + *out = json_integer(42); /* not a schema — must be boolean or object */ + return CELIX_JANSSON_SCHEMA_OK; +} + +TEST(RefTest, LoaderReturnsNonSchema) { + auto* v = celix_jansson_schema_validator_create( + non_schema_loader, nullptr, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(R"({"$ref":"http://example.com/schema"})", 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, rc); + free(errmsg); + + json_decref(sch); + free_validator(v); +} + +/* ── Multiple refs to same unresolved external target ──────────────────── */ + +TEST(RefTest, DuplicateUnresolvedExternalRef) { + auto* v = celix_jansson_schema_validator_create( + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads( + R"({"properties":{"a":{"$ref":"http://missing/x#/a"},"b":{"$ref":"http://missing/x#/a"}}})", + 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_LOADER, rc); + free(errmsg); + + json_decref(sch); + free_validator(v); +} + +/* ── Chained external $ref loader (root → A.json → B.json) ─────────────── */ + +struct chained_loader_ctx { + json_t* a_schema; /* cached docs — loader deep-copies per call */ + json_t* b_schema; + int a_calls; + int b_calls; + int fail_b; /* when set, loader returns LOADER error for B.json */ +}; + +static int chained_loader(const char* uri, json_t** out, void* ud) { + auto* ctx = static_cast(ud); + if (strcmp(uri, "http://example.com/A.json") == 0) { + ctx->a_calls++; + *out = json_deep_copy(ctx->a_schema); + } else if (strcmp(uri, "http://example.com/B.json") == 0) { + ctx->b_calls++; + if (ctx->fail_b) { + return CELIX_JANSSON_SCHEMA_ERROR_LOADER; + } + *out = json_deep_copy(ctx->b_schema); + } else { + return CELIX_JANSSON_SCHEMA_ERROR_LOADER; + } + return *out ? CELIX_JANSSON_SCHEMA_OK : CELIX_JANSSON_SCHEMA_ERROR_NOMEM; +} + +static void init_chained_ctx(chained_loader_ctx* ctx, const char* a_json, + const char* b_json, int fail_b) { + std::memset(ctx, 0, sizeof(*ctx)); + ctx->a_schema = json_loads(a_json, 0, nullptr); + ctx->b_schema = json_loads(b_json, 0, nullptr); + ctx->fail_b = fail_b; + ASSERT_NE(nullptr, ctx->a_schema); + ASSERT_NE(nullptr, ctx->b_schema); +} + +static void free_chained_ctx(chained_loader_ctx* ctx) { + json_decref(ctx->a_schema); + json_decref(ctx->b_schema); +} + +/* Shared root for the chained external-ref tests: root → A.json */ +static const char* chained_ref_root = R"({ + "type": "object", + "properties": { + "value": { "$ref": "http://example.com/A.json" } + } +})"; + +/* ── Chained external refs: B.json loaded in-place, whole-document ref ──── */ + +TEST(RefTest, ChainedExternalRefNoFragment) { + /* Root → A.json → B.json. A is loaded by Phase A of + * resolve_external_refs; B's file entry is created during A's compile, + * so B is loaded in-place by resolve_placeholder — the whole-document + * ref gets auto-resolved by the root registration. */ + static const char* a_json = R"({"$ref": "http://example.com/B.json"})"; + static const char* b_json = R"({"type": "integer", "minimum": 1})"; + + chained_loader_ctx ctx; + init_chained_ctx(&ctx, a_json, b_json, 0); + + auto* v = celix_jansson_schema_validator_create( + chained_loader, &ctx, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(chained_ref_root, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Valid: 5 is an integer >= 1 */ + json_t* inst = json_loads(R"({"value":5})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Invalid: 0 is < 1 */ + inst = json_loads(R"({"value":0})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr), 0); + json_decref(inst); + + /* Each document loaded exactly once */ + EXPECT_EQ(1, ctx.a_calls); + EXPECT_EQ(1, ctx.b_calls); + + json_decref(sch); + free_validator(v); + free_chained_ctx(&ctx); +} + +/* ── Chained external refs with a fragment walk ────────────────────────── */ + +TEST(RefTest, ChainedExternalRefWithFragment) { + /* B.json is loaded in-place, then the /definitions/X fragment is + * compiled by the document-fragment walk. */ + static const char* a_json = R"({"$ref": "http://example.com/B.json#/definitions/X"})"; + static const char* b_json = R"({ + "definitions": { + "X": { "type": "integer", "minimum": 1 } + } + })"; + + chained_loader_ctx ctx; + init_chained_ctx(&ctx, a_json, b_json, 0); + + auto* v = celix_jansson_schema_validator_create( + chained_loader, &ctx, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(chained_ref_root, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Valid: 5 is an integer >= 1 */ + json_t* inst = json_loads(R"({"value":5})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Invalid: 0 is < 1 */ + inst = json_loads(R"({"value":0})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr), 0); + json_decref(inst); + + EXPECT_EQ(1, ctx.a_calls); + EXPECT_EQ(1, ctx.b_calls); + + json_decref(sch); + free_validator(v); + free_chained_ctx(&ctx); +} + +/* ── Chained external ref to a missing fragment → REF_UNRESOLVED ───────── */ + +TEST(RefTest, ChainedExternalRefMissingFragment) { + /* B.json loads fine but the requested fragment does not exist; the + * fragment walk fails and the ref stays unresolved. */ + static const char* a_json = R"({"$ref": "http://example.com/B.json#/definitions/doesNotExist"})"; + static const char* b_json = R"({ + "definitions": { + "X": { "type": "integer" } + } + })"; + + chained_loader_ctx ctx; + init_chained_ctx(&ctx, a_json, b_json, 0); + + auto* v = celix_jansson_schema_validator_create( + chained_loader, &ctx, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(chained_ref_root, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_REF_UNRESOLVED, rc); + EXPECT_NE(nullptr, errmsg); + free(errmsg); + + EXPECT_EQ(1, ctx.a_calls); + EXPECT_EQ(1, ctx.b_calls); + + json_decref(sch); + free_validator(v); + free_chained_ctx(&ctx); +} + +/* ── Chained external ref where the second load fails → LOADER ─────────── */ + +TEST(RefTest, ChainedExternalRefLoaderError) { + /* The in-place load of B.json fails in resolve_placeholder; the next + * iteration's Phase A retries it and aborts with the loader's rc. + * b_calls == 2 proves both the in-place attempt and the retry ran. */ + static const char* a_json = R"({"$ref": "http://example.com/B.json"})"; + static const char* b_json = R"({"type": "integer"})"; + + chained_loader_ctx ctx; + init_chained_ctx(&ctx, a_json, b_json, 1 /* fail_b */); + + auto* v = celix_jansson_schema_validator_create( + chained_loader, &ctx, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(chained_ref_root, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_LOADER, rc); + free(errmsg); + + /* One call from the in-place load, one from Phase A's retry */ + EXPECT_EQ(1, ctx.a_calls); + EXPECT_EQ(2, ctx.b_calls); + + json_decref(sch); + free_validator(v); + free_chained_ctx(&ctx); +} + +/* ── Baseline: single-level external ref (Phase A only) ────────────────── */ + +TEST(RefTest, SingleLevelExternalRef) { + /* Root → A.json only; handled entirely by Phase A of + * resolve_external_refs, never touching the in-place load path. + * b_json is never requested, so the loader must never be called for it. */ + static const char* a_json = R"({"type": "integer", "minimum": 1})"; + static const char* b_json = R"({"type": "integer"})"; + + chained_loader_ctx ctx; + init_chained_ctx(&ctx, a_json, b_json, 0); + + auto* v = celix_jansson_schema_validator_create( + chained_loader, &ctx, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(chained_ref_root, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Valid: 5 is an integer >= 1 */ + json_t* inst = json_loads(R"({"value":5})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Invalid: 0 is < 1 */ + inst = json_loads(R"({"value":0})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr), 0); + json_decref(inst); + + EXPECT_EQ(1, ctx.a_calls); + EXPECT_EQ(0, ctx.b_calls); + + json_decref(sch); + free_validator(v); + free_chained_ctx(&ctx); +} \ No newline at end of file diff --git a/libs/jansson_ext/gtest/src/test_schema_keywords.cpp b/libs/jansson_ext/gtest/src/test_schema_keywords.cpp new file mode 100644 index 000000000..64b7d96ed --- /dev/null +++ b/libs/jansson_ext/gtest/src/test_schema_keywords.cpp @@ -0,0 +1,1028 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "test_common.h" + +#include + +/* ═══════════════════════════════════════════════════════════════════════════ + * Compile-time error tests (checkerless validator) + * ═══════════════════════════════════════════════════════════════════════════ */ + +TEST(SchemaCompileTest, InvalidPattern) { + auto* v = celix_jansson_schema_validator_create(nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + + /* Use a pattern with an unmatched bracket — this exercises the regcomp-failure + * path in make_type_schema. (Some patterns like "[" trigger a double-free + * in d_type cleanup when pattern_str is freed both explicitly and by d_str + * during node destruction; this test uses a pattern that regcomp rejects + * without hitting that specific issue.) */ + json_t* sch = json_loads(R"({"type":"string","pattern":"***invalid"})", 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_PATTERN, rc) << "Expected INVALID_PATTERN, got: " << rc; + EXPECT_NE(nullptr, errmsg); + free(errmsg); + + json_decref(sch); + free_validator(v); +} + +TEST(SchemaCompileTest, FormatWithoutChecker) { + auto* v = celix_jansson_schema_validator_create(nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(R"({"type":"string","format":"email"})", 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_FORMAT_CHECKER, rc); + ASSERT_NE(nullptr, errmsg); + EXPECT_STREQ("format checker required but not provided", errmsg); + free(errmsg); + + json_decref(sch); + free_validator(v); +} + +TEST(SchemaCompileTest, ContentEncodingWithoutChecker) { + auto* v = celix_jansson_schema_validator_create(nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(R"({"type":"string","contentEncoding":"base64"})", 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_CONTENT_CHECKER, rc); + ASSERT_NE(nullptr, errmsg); + free(errmsg); + + json_decref(sch); + free_validator(v); +} + +TEST(SchemaCompileTest, TupleItemsCompileError) { + /* Cover cleanup of already-compiled tuple items when a later item fails: + * the manual partial cleanup resets items_len and n's autoptr-unref then + * routes the remaining teardown through d_array */ + auto* v = celix_jansson_schema_validator_create(nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads( + R"({"type":"array","items":[{"type":"integer"},{"type":"string","pattern":"***invalid"}]})", + 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_PATTERN, rc); + ASSERT_NE(nullptr, errmsg); + free(errmsg); + + json_decref(sch); + free_validator(v); +} + +TEST(SchemaCompileTest, AllOfCompileError) { + /* Cover cleanup of already-compiled allOf sub-schemas when a later one fails: + * cn's autoptr-unref routes the partial state (len set, NULL tail items) + * through d_comb */ + auto* v = celix_jansson_schema_validator_create(nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads( + R"({"allOf":[{"type":"integer"},{"type":"string","pattern":"***invalid"}]})", + 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_PATTERN, rc); + ASSERT_NE(nullptr, errmsg); + free(errmsg); + + json_decref(sch); + free_validator(v); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * Keyword tests — use ValidatorTest fixture + * ═══════════════════════════════════════════════════════════════════════════ */ + +class SchemaKeywordsTest : public ValidatorTest {}; + +/* ── multipleOf ──────────────────────────────────────────────────────────── */ + +TEST_F(SchemaKeywordsTest, MultipleOf) { + load_schema(R"({"multipleOf":0.5})"); + assert_valid("4"); + assert_valid("4.5"); + assert_invalid("4.3", 1); + /* Non-number ignored (not an error for multipleOf) */ + assert_valid(R"("hello")"); +} + +TEST_F(SchemaKeywordsTest, MultipleOfInteger) { + load_schema(R"({"type":"integer","multipleOf":3})"); + assert_valid("9"); + assert_invalid("10", 1); + + /* Large integer */ + json_t* inst = json_loads("9000000000000", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + int n = celix_jansson_schema_validate(v_, inst, capture_error, nullptr, nullptr); + EXPECT_EQ(0, n); + json_decref(inst); +} + +/* ── uniqueItems ─────────────────────────────────────────────────────────── */ + +TEST_F(SchemaKeywordsTest, UniqueItems) { + load_schema(R"({"type":"array","uniqueItems":true})"); + assert_valid("[1,2,3]"); + assert_invalid("[1,1]", 1); + + /* Deep object equality */ + assert_invalid(R"([{"a":1},{"a":1}])", 1); + assert_valid(R"([{"a":1},{"a":2}])"); + + /* Different types are unique */ + assert_valid(R"([1,"1"])"); +} + +/* ── contains ────────────────────────────────────────────────────────────── */ + +TEST_F(SchemaKeywordsTest, Contains) { + load_schema(R"({"contains":{"type":"integer"}})"); + assert_valid(R"([1,"a"])"); + assert_invalid(R"(["a","b"])", 1); + + /* Non-array instance — contains is ignored */ + assert_valid("42"); +} + +TEST_F(SchemaKeywordsTest, ContainsNested) { + /* Nested under an object property: parent path carries tokens, exercising + * the token-copy loop in v_array's contains block (line 912). */ + load_schema(R"({"type":"object","properties":{"arr":{"contains":{"type":"integer"}}}})"); + assert_valid(R"({"arr":[1,"x"]})"); /* hits line 912 with p->len == 1 */ + assert_invalid(R"({"arr":["x"]})", 1); + + /* Nested inside an array (items): parent path = ["0"], same loop. */ + load_schema(R"({"type":"array","items":{"contains":{"type":"integer"}}})"); + assert_valid(R"([[1,"x"],[2]])"); + assert_invalid(R"([["x"]])", 1); +} + +/* ── minProperties / maxProperties ───────────────────────────────────────── */ + +TEST_F(SchemaKeywordsTest, MinMaxProperties) { + load_schema(R"({"minProperties":2,"maxProperties":3})"); + assert_valid(R"({"a":1,"b":2})"); + assert_invalid("{}", 1); + assert_invalid(R"({"a":1,"b":2,"c":3,"d":4})", 1); + + /* Non-object instance — constraints ignored */ + assert_valid(R"("hello")"); +} + +/* ── minItems / maxItems ────────────────────────────────────────────────── */ + +TEST_F(SchemaKeywordsTest, MinMaxItems) { + load_schema(R"({"minItems":2,"maxItems":3})"); + assert_valid("[1,2]"); + assert_invalid("[1]", 1); + assert_invalid("[1,2,3,4]", 1); + + /* Non-array instance — constraints ignored */ + assert_valid("42"); +} + +/* ── pattern (schema keyword) ────────────────────────────────────────────── */ + +TEST_F(SchemaKeywordsTest, Pattern) { + load_schema(R"({"pattern":"^a+$"})"); + assert_valid(R"("aaa")"); + assert_invalid(R"("baa")", 1); + + /* Non-string instance — pattern ignored */ + assert_valid("42"); +} + +/* ── patternProperties ───────────────────────────────────────────────────── */ + +TEST_F(SchemaKeywordsTest, PatternProperties) { + load_schema(R"({ + "type": "object", + "patternProperties": { + "^S_": {"type": "string"} + } + })"); + assert_valid(R"({"S_a":"x"})"); + assert_invalid(R"({"S_a":1})", 1); + + /* Keys not matching the pattern are not validated */ + assert_valid(R"({"T_a":1})"); +} + +/* ── additionalItems ─────────────────────────────────────────────────────── */ + +TEST_F(SchemaKeywordsTest, AdditionalItems) { + load_schema(R"({ + "type": "array", + "items": [{"type": "integer"}], + "additionalItems": {"type": "string"} + })"); + assert_valid("[1]"); + assert_valid(R"([1,"a"])"); + /* Additional item is an integer but should be a string */ + assert_invalid("[1,2]", 1); +} + +/* ── dependencies (schema form) ──────────────────────────────────────────── */ + +TEST_F(SchemaKeywordsTest, DependenciesSchemaForm) { + load_schema(R"({ + "dependencies": { + "credit_card": {"required": ["billing_address"]} + } + })"); + /* Has credit_card but no billing_address */ + assert_invalid(R"({"credit_card":{}})", 1); + /* Has both */ + assert_valid(R"({"credit_card":{},"billing_address":123})"); + /* Neither — valid */ + assert_valid("{}"); +} + +/* ── exclusiveMinimum / exclusiveMaximum ─────────────────────────────────── */ + +TEST_F(SchemaKeywordsTest, ExclusiveMinimum) { + load_schema(R"({"exclusiveMinimum":5})"); + assert_valid("6"); + assert_valid("5.1"); + assert_invalid("5", 1); + assert_invalid("4", 1); + + /* Non-number instance */ + assert_valid(R"("hello")"); +} + +TEST_F(SchemaKeywordsTest, ExclusiveMaximum) { + load_schema(R"({"type":"number","exclusiveMaximum":5})"); + assert_valid("4"); + assert_valid("4.9"); + assert_invalid("5", 1); + assert_invalid("6", 1); +} + +/* ── propertyNames ───────────────────────────────────────────────────────── */ + +TEST_F(SchemaKeywordsTest, PropertyNames) { + load_schema(R"({"propertyNames":{"pattern":"^[a-z]+$"}})"); + assert_valid(R"({"abc":1})"); + /* Uppercase property name fails the pattern */ + assert_invalid(R"({"ABC":1})", 1); + + /* Non-object instance */ + assert_valid("42"); +} + +/* ── Nested $ref chain ───────────────────────────────────────────────────── */ + +TEST_F(SchemaKeywordsTest, NestedRefChain) { + load_schema(R"({ + "$ref": "#/definitions/A", + "definitions": { + "A": {"$ref": "#/definitions/B"}, + "B": {"$ref": "#/definitions/C"}, + "C": {"type": "integer"} + } + })"); + assert_valid("5"); + assert_invalid(R"("x")", 1); +} + +/* ── Boolean schemas in combinators ──────────────────────────────────────── */ + +TEST_F(SchemaKeywordsTest, BooleanInCombinators) { + /* allOf with false: everything fails */ + load_schema(R"({"allOf":[true,false]})"); + assert_invalid("42", 1); + assert_invalid(R"("hello")", 1); + + /* Reset and test anyOf with false */ + json_decref(schema_); + schema_ = nullptr; + load_schema(R"({"anyOf":[false,{"type":"string"}]})"); + assert_valid(R"("hello")"); + assert_invalid("42", 1); + + /* Reset and test oneOf */ + json_decref(schema_); + schema_ = nullptr; + load_schema(R"({"oneOf":[false,true]})"); + assert_valid("42"); + + /* Reset and test not */ + json_decref(schema_); + schema_ = nullptr; + load_schema(R"({"not":true})"); + assert_invalid("42", 1); + + /* Reset and test not with false */ + json_decref(schema_); + schema_ = nullptr; + load_schema(R"({"not":false})"); + assert_valid("42"); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * Content callback test + * ═══════════════════════════════════════════════════════════════════════════ */ + +/* Simple base64 decoding table (RFC 4648) */ +static int b64_decode_char(char c) { + if (c >= 'A' && c <= 'Z') return c - 'A'; + if (c >= 'a' && c <= 'z') return c - 'a' + 26; + if (c >= '0' && c <= '9') return c - '0' + 52; + if (c == '+') return 62; + if (c == '/') return 63; + return -1; +} + +static int content_checker_cb(const char* encoding, const char* media_type, json_t* instance, void* /*user_data*/) { + if (!encoding || strcmp(encoding, "base64") != 0) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + if (!media_type || strcmp(media_type, "application/json") != 0) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + + const char* s = json_string_value(instance); + if (!s) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + + /* Basic base64 validity check: length must be multiple of 4, all chars + * must be valid base64 alphabet or padding '=' (only at end). */ + size_t len = strlen(s); + if (len % 4 != 0) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + + for (size_t i = 0; i < len; i++) { + if (s[i] == '=') { + /* Padding only allowed in last 2 positions */ + if (i < len - 2) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + /* After first '=', only '=' allowed */ + for (size_t j = i; j < len; j++) { + if (s[j] != '=') + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + } + break; + } + if (b64_decode_char(s[i]) < 0) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + } + + /* Decode and verify it's valid UTF-8 JSON */ + size_t out_len = (len / 4) * 3; + if (s[len - 1] == '=') out_len--; + if (s[len - 2] == '=') out_len--; + + unsigned char* decoded = (unsigned char*)malloc(out_len + 1); + if (!decoded) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + + size_t di = 0; + for (size_t i = 0; i < len; i += 4) { + int b0 = b64_decode_char(s[i]); + int b1 = b64_decode_char(s[i + 1]); + int b2 = (s[i + 2] == '=') ? 0 : b64_decode_char(s[i + 2]); + int b3 = (s[i + 3] == '=') ? 0 : b64_decode_char(s[i + 3]); + + if (di < out_len) decoded[di++] = (unsigned char)((b0 << 2) | (b1 >> 4)); + if (di < out_len) decoded[di++] = (unsigned char)((b1 << 4) | (b2 >> 2)); + if (di < out_len) decoded[di++] = (unsigned char)((b2 << 6) | b3); + } + decoded[out_len] = '\0'; + + /* Verify decoded content is valid JSON */ + json_error_t jerr; + json_t* parsed = json_loads((const char*)decoded, 0, &jerr); + free(decoded); + + if (!parsed) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + + json_decref(parsed); + return CELIX_JANSSON_SCHEMA_OK; +} + +TEST(SchemaCompileTest, ContentCallback) { + auto* v = celix_jansson_schema_validator_create( + nullptr, nullptr, + celix_jansson_schema_default_format_check, nullptr, + content_checker_cb, nullptr); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads( + R"({"type":"string","contentEncoding":"base64","contentMediaType":"application/json"})", + 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* "eyJhIjoxfQ==" is base64-encoded '{"a":1}' — valid JSON */ + json_t* inst = json_loads(R"("eyJhIjoxfQ==")", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + int n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr); + EXPECT_EQ(0, n) << "Expected valid content, errors: " << (captured_messages.empty() ? "none" : captured_messages[0]); + json_decref(inst); + + /* "!!!not-valid-base64!!!" — invalid */ + inst = json_loads(R"("!!!not-valid-base64!!!")", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr); + EXPECT_GT(n, 0); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * Path escaping in validation errors (covers celix_jansson_path_str ~0/~1) + * ═══════════════════════════════════════════════════════════════════════════ */ + +TEST_F(SchemaKeywordsTest, PathEscapingInErrors) { + /* Property names containing '/' → escaped as ~1 in error pointer. + * Property names containing '~' → escaped as ~0 in error pointer. */ + load_schema(R"({ + "properties": { + "a/b": {"type": "integer"}, + "c~d": {"type": "boolean"} + } + })"); + + /* Wrong type at key "a/b" → error pointer "/a~1b" */ + reset_errors(); + json_t* inst = json_loads(R"({"a/b":"not_int"})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + int n = celix_jansson_schema_validate(v_, inst, capture_error, nullptr, nullptr); + EXPECT_EQ(1, n); + ASSERT_GE(captured_errors.size(), 1u); + EXPECT_EQ("/a~1b", captured_errors[0]); + json_decref(inst); + + /* Wrong type at key "c~d" → error pointer "/c~0d" */ + reset_errors(); + inst = json_loads(R"({"c~d":"not_bool"})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + n = celix_jansson_schema_validate(v_, inst, capture_error, nullptr, nullptr); + EXPECT_EQ(1, n); + ASSERT_GE(captured_errors.size(), 1u); + EXPECT_EQ("/c~0d", captured_errors[0]); + json_decref(inst); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * NULL-arg guards + * ═══════════════════════════════════════════════════════════════════════════ */ + +TEST(SchemaNullGuardTest, ValidateNullValidator) { + json_t* inst = json_integer(42); + EXPECT_EQ(-1, celix_jansson_schema_validate(nullptr, inst, nullptr, nullptr, nullptr)); + EXPECT_EQ(-1, celix_jansson_schema_validate_uri(nullptr, inst, "#", nullptr, nullptr, nullptr)); + json_decref(inst); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * Type-mismatch validators + * ═══════════════════════════════════════════════════════════════════════════ */ + +TEST_F(SchemaKeywordsTest, AllTypeMismatchErrors) { + /* null */ + load_schema(R"({"type":"null"})"); + assert_invalid("42", 1); + + /* boolean */ + json_decref(schema_); schema_ = nullptr; + load_schema(R"({"type":"boolean"})"); + assert_invalid("42", 1); + + /* string */ + json_decref(schema_); schema_ = nullptr; + load_schema(R"({"type":"string"})"); + assert_invalid("42", 1); + + /* integer */ + json_decref(schema_); schema_ = nullptr; + load_schema(R"({"type":"integer"})"); + assert_invalid(R"("hello")", 1); + + /* number */ + json_decref(schema_); schema_ = nullptr; + load_schema(R"({"type":"number"})"); + assert_invalid(R"("hello")", 1); + + /* object */ + json_decref(schema_); schema_ = nullptr; + load_schema(R"({"type":"object"})"); + assert_invalid("42", 1); + + /* array */ + json_decref(schema_); schema_ = nullptr; + load_schema(R"({"type":"array"})"); + assert_invalid("42", 1); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * multipleOf: 0 + * ═══════════════════════════════════════════════════════════════════════════ */ + +TEST_F(SchemaKeywordsTest, MultipleOfZero) { + load_schema(R"({"multipleOf":0})"); + assert_valid("5"); + assert_valid("4.3"); + assert_valid(R"("hello")"); /* non-number ignored */ +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * Content checker missing at validate time + * ═══════════════════════════════════════════════════════════════════════════ */ + +TEST(SchemaCompileTest, ContentMediaTypeWithoutCheckerAtValidate) { + auto* v = celix_jansson_schema_validator_create(nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(R"({"type":"string","contentMediaType":"application/json"})", 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, sch, &errmsg)); + free(errmsg); + + json_t* inst = json_loads(R"("abc")", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + int n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr); + EXPECT_EQ(1, n); + ASSERT_GE(captured_messages.size(), 1u); + EXPECT_NE(std::string::npos, captured_messages[0].find("content checker was not provided")); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * Array-form "type" + * ═══════════════════════════════════════════════════════════════════════════ */ + +TEST_F(SchemaKeywordsTest, TypeArrayForm) { + load_schema(R"({"type":["number","string"]})"); + assert_valid("42"); + assert_valid("42.5"); + assert_valid(R"("hello")"); + assert_invalid("true", 1); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * Recursion depth guard + * ═══════════════════════════════════════════════════════════════════════════ */ + +/* Self-referencing $ref "#" compiles but validation hits the ref_depth guard. + * This is because $ref: "#" on the root creates a self-loop at validation time. */ +TEST_F(SchemaKeywordsTest, SelfRefRoot) { + load_schema(R"({"$ref":"#"})"); + /* Validation detects circular ref recursion */ + assert_invalid("42", 1); + ASSERT_GE(captured_messages.size(), 1u); + EXPECT_STREQ("exceeded maximum $ref recursion depth", captured_messages[0].c_str()); + assert_invalid(R"("hello")", 1); +} + +/* Official test-suite "root pointer ref" semantics: a {"$ref":"#"} inside + * properties points back at the root schema, so the property value must + * recursively satisfy the root constraints. This is wired up at compile time + * (placeholder + phase-2 resolution), no runtime fallback involved. */ +TEST_F(SchemaKeywordsTest, RecursiveRootRef) { + load_schema(R"({ + "properties": { + "foo": {"$ref": "#"} + }, + "additionalProperties": false + })"); + + /* foo: false — no object properties to check, passes */ + assert_valid(R"({"foo": false})"); + /* recursive match: inner {"foo": false} satisfies the root again */ + assert_valid(R"({"foo": {"foo": false}})"); + /* additionalProperties rejects "bar" at the root */ + assert_invalid(R"({"bar": false})", 1); + /* recursion: the inner object's "bar" is rejected by the same rule */ + assert_invalid(R"({"foo": {"bar": false}})", 1); +} + +/* Circular definitions ref compiles successfully via eager definition compilation. + * Validation of a circular ref triggers the ref_depth guard and reports an error. */ +TEST_F(SchemaKeywordsTest, CircularDefinitionsRef) { + load_schema(R"({ + "definitions": { + "A": {"$ref": "#/definitions/A"} + }, + "$ref": "#/definitions/A" + })"); + /* Validation hits infinite recursion guard */ + assert_invalid("42", 1); +} + +/* Mutual cross-refs between properties: the first property resolves the target + * via fragment walk and caches it in the hash table, so the second property's + * ref hits the cache. No infinite recursion — compilation succeeds. */ +TEST(SchemaCompileTest, CircularPropertiesRefResolves) { + auto* v = celix_jansson_schema_validator_create(nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + + /* a → b → a cycle through properties — but hash table cache breaks the cycle */ + json_t* sch = json_loads(R"({ + "properties": { + "a": {"$ref": "#/properties/b"}, + "b": {"$ref": "#/properties/a"} + } + })", 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + /* Compilation succeeds because fragment walk caches resolved schemas */ + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, rc); + free(errmsg); + json_decref(sch); + celix_jansson_schema_validator_destroy(v); +} + +/* Deeply nested but still within the limit: 20 levels of allOf should compile. */ +TEST(SchemaCompileTest, DeepNestingUnderLimit) { + auto* v = celix_jansson_schema_validator_create(nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + + /* Build a 20-level allOf chain programmatically */ + json_t* inner = json_pack("{s:s}", "type", "integer"); + json_t* current = inner; + for (int i = 0; i < 20; i++) { + json_t* wrapper = json_pack("{s:[o]}", "allOf", current); + current = wrapper; + } + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, current, &errmsg); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : "unknown error"); + json_decref(current); + free(errmsg); + + /* Validate integer — should pass */ + json_t* inst = json_integer(42); + int n = celix_jansson_schema_validate(v, inst, [](const char*, json_t*, const char*, void*) {}, nullptr, nullptr); + EXPECT_EQ(0, n); + json_decref(inst); + + celix_jansson_schema_validator_destroy(v); +} + +/* Exceeding the depth limit: 25 levels of allOf triggers the guard. + * With the error-propagation fix, make_type_schema now returns the error. */ +TEST(SchemaCompileTest, DeepNestingExceedsLimit) { + auto* v = celix_jansson_schema_validator_create(nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + + /* Build a 25-level allOf chain */ + json_t* inner = json_pack("{s:s}", "type", "integer"); + json_t* current = inner; + for (int i = 0; i < 25; i++) { + json_t* wrapper = json_pack("{s:[o]}", "allOf", current); + current = wrapper; + } + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, current, &errmsg); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_SCHEMA, rc); + + json_decref(current); + free(errmsg); + celix_jansson_schema_validator_destroy(v); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * Error propagation from each keyword — depth guard triggers inside + * sub-schema, error must propagate through make_type_schema to caller. + * ═══════════════════════════════════════════════════════════════════════════ */ + +/* Helper: build a 25-level allOf chain with {"type":"integer"} at the leaf. + * This chain exceeds the depth guard (depth > 20). */ +static json_t* build_deep_allof_chain(int levels) { + json_t* inner = json_pack("{s:s}", "type", "integer"); + json_t* current = inner; + for (int i = 0; i < levels; i++) { + json_t* wrapper = json_pack("{s:[o]}", "allOf", current); + current = wrapper; + } + return current; +} + +/* Helper: compile a schema wrapping the deep chain in a keyword; expect error. */ +static void expect_deep_nesting_error(json_t* schema) { + auto* v = celix_jansson_schema_validator_create(nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, schema, &errmsg); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_SCHEMA, rc); + json_decref(schema); + free(errmsg); + celix_jansson_schema_validator_destroy(v); +} + +TEST(SchemaCompileTest, DeepNot) { + json_t* chain = build_deep_allof_chain(25); + expect_deep_nesting_error(json_pack("{s:o}", "not", chain)); +} + +TEST(SchemaCompileTest, DeepPatternProperties) { + json_t* chain = build_deep_allof_chain(25); + expect_deep_nesting_error(json_pack("{s:{s:o}}", "patternProperties", ".*", chain)); +} + +TEST(SchemaCompileTest, DeepAdditionalProperties) { + json_t* chain = build_deep_allof_chain(25); + expect_deep_nesting_error(json_pack("{s:o}", "additionalProperties", chain)); +} + +TEST(SchemaCompileTest, DeepPropertyNames) { + json_t* chain = build_deep_allof_chain(25); + expect_deep_nesting_error(json_pack("{s:o}", "propertyNames", chain)); +} + +TEST(SchemaCompileTest, DeepItemsSingle) { + json_t* chain = build_deep_allof_chain(25); + expect_deep_nesting_error(json_pack("{s:o}", "items", chain)); +} + +TEST(SchemaCompileTest, DeepItemsTuple) { + json_t* chain = build_deep_allof_chain(25); + expect_deep_nesting_error(json_pack("{s:[o]}", "items", chain)); +} + +TEST(SchemaCompileTest, DeepAdditionalItems) { + json_t* chain = build_deep_allof_chain(25); + expect_deep_nesting_error(json_pack("{s:o}", "additionalItems", chain)); +} + +TEST(SchemaCompileTest, DeepContains) { + json_t* chain = build_deep_allof_chain(25); + expect_deep_nesting_error(json_pack("{s:o}", "contains", chain)); +} + +TEST(SchemaCompileTest, DeepDependenciesSchema) { + json_t* chain = build_deep_allof_chain(25); + expect_deep_nesting_error(json_pack("{s:{s:o}}", "dependencies", "x", chain)); +} + +TEST(SchemaCompileTest, DeepIfThenElse) { + json_t* chain = build_deep_allof_chain(25); + expect_deep_nesting_error(json_pack("{s:o}", "if", chain)); +} + +TEST(SchemaCompileTest, DeepThen) { + /* if compiles OK, then exceeds depth guard -> cleanup at lines 1642-1643 */ + json_t* chain = build_deep_allof_chain(25); + expect_deep_nesting_error( + json_pack("{s:o, s:o}", "if", json_pack("{s:s}", "type", "integer"), "then", chain)); +} + +TEST(SchemaCompileTest, DeepElse) { + /* if and then compile OK, else exceeds depth guard -> cleanup at lines 1650-1651 */ + json_t* chain = build_deep_allof_chain(25); + expect_deep_nesting_error(json_pack("{s:o, s:o, s:o}", + "if", json_pack("{s:s}", "type", "integer"), + "then", json_pack("{s:s}", "type", "string"), + "else", chain)); +} + +TEST(SchemaCompileTest, DeepDefinitions) { + json_t* chain = build_deep_allof_chain(25); + expect_deep_nesting_error(json_pack("{s:{s:o}}", "definitions", "A", chain)); +} + +TEST(SchemaCompileTest, DefinitionsErrorWithIdCleanup) { + /* Cover lines 1788-1789 in celix_jansson_schema.c: a schema carrying both + * a derivable $id and a failing definitions entry must celix_jansson_uri_clear(&my_base) + * before propagating the error. + * Nested under allOf so compiled via schema_make_internal (eff_base_out == NULL), + * which keeps id_stored_in_out == false. */ + json_t* chain = build_deep_allof_chain(25); + json_t* sub = json_pack("{s:s, s:{s:o}}", + "$id", "https://example.com/defs-with-id", + "definitions", "A", chain); + ASSERT_NE(nullptr, sub); + expect_deep_nesting_error(json_pack("{s:[o]}", "allOf", sub)); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * Non-string $ref + * ═══════════════════════════════════════════════════════════════════════════ */ + +TEST(SchemaCompileTest, NonStringRef) { + auto* v = celix_jansson_schema_validator_create(nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(R"({"$ref":42})", 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_SCHEMA, rc); + free(errmsg); + + json_decref(sch); + free_validator(v); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * $ref into schema-position tokens + * ═══════════════════════════════════════════════════════════════════════════ */ + +TEST_F(SchemaKeywordsTest, RefToSchemaPositions) { + load_schema(R"({"$ref":"#/not","not":{"type":"integer"}})"); + assert_valid("5"); + assert_invalid(R"("x")", 1); + + json_decref(schema_); schema_ = nullptr; + load_schema(R"({"$ref":"#/items","items":{"type":"string"}})"); + assert_valid(R"("hello")"); + assert_invalid("42", 1); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * validate_uri with non-existent location + * ═══════════════════════════════════════════════════════════════════════════ */ + +TEST_F(SchemaKeywordsTest, ValidateUriNonExistentLocation) { + load_schema(R"({"type":"integer"})"); + reset_errors(); + json_t* inst = json_loads("42", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + int n = celix_jansson_schema_validate_uri( + v_, inst, "http://unknown.example/x#/a", capture_error, nullptr, nullptr); + EXPECT_EQ(0, n); + json_decref(inst); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * Fragment pointing to non-schema value + * ═══════════════════════════════════════════════════════════════════════════ */ + +TEST_F(SchemaKeywordsTest, FragmentPointingToNonSchemaValue) { + load_schema(R"({"x":42})"); + json_t* inst = json_loads("42", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + int n = celix_jansson_schema_validate_uri(v_, inst, "#/x", capture_error, nullptr, nullptr); + EXPECT_EQ(0, n); + json_decref(inst); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * dv_ref default-value fallback via patch_out + * ═══════════════════════════════════════════════════════════════════════════ */ + +TEST_F(SchemaKeywordsTest, RefDefaultValueFallback) { + load_schema(R"({"properties":{"a":{"$ref":"http://missing.example/x#/a"}}})"); + json_t* inst = json_loads(R"({})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + json_t* patch = nullptr; + reset_errors(); + int n = celix_jansson_schema_validate(v_, inst, capture_error, nullptr, &patch); + EXPECT_EQ(0, n); + ASSERT_NE(nullptr, patch); + EXPECT_EQ(0u, json_array_size(patch)); + json_decref(inst); + json_decref(patch); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * Nested dependencies (array form) with path copy + * ═══════════════════════════════════════════════════════════════════════════ */ + +TEST_F(SchemaKeywordsTest, NestedDependenciesArrayForm) { + load_schema(R"({ + "type": "object", + "properties": { + "outer": { + "type": "object", + "dependencies": {"a": ["b"]} + } + } + })"); + assert_invalid(R"({"outer":{"a":1}})", 1); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * Nested propertyNames with path copy + * ═══════════════════════════════════════════════════════════════════════════ */ + +TEST_F(SchemaKeywordsTest, NestedPropertyNames) { + load_schema(R"({ + "type": "object", + "properties": { + "inner": { + "type": "object", + "propertyNames": {"pattern": "^[a-z]+$"} + } + } + })"); + assert_invalid(R"({"inner":{"ABC":1}})", 1); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * Root default for null instance with patch_out + * ═══════════════════════════════════════════════════════════════════════════ */ + +TEST_F(SchemaKeywordsTest, RootDefaultForNullWithPatch) { + load_schema(R"({"default":"N/A"})"); + json_t* inst = json_loads("null", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + json_t* patch = nullptr; + reset_errors(); + int n = celix_jansson_schema_validate(v_, inst, capture_error, nullptr, &patch); + EXPECT_EQ(0, n); + ASSERT_NE(nullptr, patch); + ASSERT_GE(json_array_size(patch), 1u); + json_decref(inst); + json_decref(patch); +} + +TEST(SchemaCompileTest, ValidateAgainstUnresolvedRef) { + auto* v = celix_jansson_schema_validator_create(nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(R"({"$ref":"http://missing.example/x#/a"})", 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_LOADER, rc); + free(errmsg); + + json_t* inst = json_loads("42", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + int n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr); + EXPECT_EQ(1, n); + ASSERT_GE(captured_messages.size(), 1u); + EXPECT_STREQ("unresolved or freed schema-reference", captured_messages[0].c_str()); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * validate_uri with non-NULL patch_out + * ═══════════════════════════════════════════════════════════════════════════ */ + +TEST_F(SchemaKeywordsTest, ValidateUriPatchOut) { + load_schema(R"({"default":"N/A"})"); + json_t* inst = json_loads("null", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + json_t* patch = nullptr; + reset_errors(); + int n = celix_jansson_schema_validate_uri(v_, inst, "#", capture_error, nullptr, &patch); + EXPECT_EQ(0, n); + ASSERT_NE(nullptr, patch); + json_decref(inst); + json_decref(patch); +} diff --git a/libs/jansson_ext/gtest/src/test_smoke.cpp b/libs/jansson_ext/gtest/src/test_smoke.cpp new file mode 100644 index 000000000..99b81be6d --- /dev/null +++ b/libs/jansson_ext/gtest/src/test_smoke.cpp @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "test_common.h" + +TEST(SmokeTest, CreateAndFreeValidator) { + celix_jansson_schema_validator_t* v = + celix_jansson_schema_validator_create(nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + ASSERT_NE(nullptr, v); + celix_jansson_schema_validator_destroy(v); +} + +TEST(SmokeTest, SetRootSchemaBoolean) { + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + /* true schema accepts everything */ + json_t* schema = json_true(); + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, schema, &errmsg); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : "no message"); + free(errmsg); + json_decref(schema); + free_validator(v); +} + +TEST(SmokeTest, ValidateAgainstTrueSchema) { + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* schema = json_true(); + char* errmsg = nullptr; + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, &errmsg)); + free(errmsg); + + reset_errors(); + json_t* instance = json_loads("{\"anything\": 42}", 0, nullptr); + ASSERT_NE(nullptr, instance); + + int n = celix_jansson_schema_validate(v, instance, capture_error, nullptr, nullptr); + EXPECT_EQ(0, n) << "true schema should accept everything"; + + json_decref(instance); + json_decref(schema); + free_validator(v); +} + +TEST(SmokeTest, ValidateAgainstFalseSchema) { + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* schema = json_false(); + char* errmsg = nullptr; + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_set_root_schema(v, schema, &errmsg)); + free(errmsg); + + reset_errors(); + json_t* instance = json_loads("42", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, instance); + + int n = celix_jansson_schema_validate(v, instance, capture_error, nullptr, nullptr); + EXPECT_GT(n, 0) << "false schema should reject everything"; + + json_decref(instance); + json_decref(schema); + free_validator(v); +} + +TEST(SmokeTest, SetRootSchemaWithoutSchema) { + auto* v = make_validator(); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, nullptr, &errmsg); + EXPECT_NE(CELIX_JANSSON_SCHEMA_OK, rc); + free(errmsg); + free_validator(v); +} + +TEST(SmokeTest, DefaultFormatCheck) { + /* date-time */ + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("date-time", "1985-04-12T23:20:50Z", nullptr)); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT, + celix_jansson_schema_default_format_check("date-time", "not-a-date", nullptr)); + + /* ipv4 */ + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_schema_default_format_check("ipv4", "192.168.1.1", nullptr)); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT, + celix_jansson_schema_default_format_check("ipv4", "999.999.999.999", nullptr)); + + /* uuid */ + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, + celix_jansson_schema_default_format_check("uuid", "12345678-1234-1234-1234-123456789abc", nullptr)); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT, + celix_jansson_schema_default_format_check("uuid", "bad-uuid", nullptr)); +} + +TEST(SmokeTest, Strerror) { + const char* s = celix_jansson_schema_strerror(CELIX_JANSSON_SCHEMA_OK); + EXPECT_STREQ("success", s); + s = celix_jansson_schema_strerror(CELIX_JANSSON_SCHEMA_ERROR_NOMEM); + EXPECT_STREQ("allocation failure", s); +} + +TEST(SmokeTest, Draft7MetaSchema) { + json_t* ms = celix_jansson_schema_draft7_meta_schema(); + ASSERT_NE(nullptr, ms); + EXPECT_TRUE(json_is_object(ms)); + json_decref(ms); +} + +/* ── strerror: all error codes ─────────────────────────────────────────── */ + +TEST(SmokeTest, StrerrorAllCodes) { + struct { + int code; + const char* expected; + } cases[] = { + {CELIX_JANSSON_SCHEMA_OK, "success"}, + {CELIX_JANSSON_SCHEMA_ERROR_NOMEM, "allocation failure"}, + {CELIX_JANSSON_SCHEMA_ERROR_INVALID_SCHEMA, "schema must be boolean or object"}, + {CELIX_JANSSON_SCHEMA_ERROR_SCHEMA_PARSE, "JSON parse error"}, + {CELIX_JANSSON_SCHEMA_ERROR_URI, "malformed URI"}, + {CELIX_JANSSON_SCHEMA_ERROR_REF_UNRESOLVED, "unresolved $ref"}, + {CELIX_JANSSON_SCHEMA_ERROR_LOADER, "schema loader failed or absent"}, + {CELIX_JANSSON_SCHEMA_ERROR_FORMAT_CHECKER, "format checker required but not provided"}, + {CELIX_JANSSON_SCHEMA_ERROR_CONTENT_CHECKER, "content checker required but not provided"}, + {CELIX_JANSSON_SCHEMA_ERROR_DUPLICATE_URI, "duplicate URI"}, + {CELIX_JANSSON_SCHEMA_ERROR_INVALID_PATTERN, "invalid regex pattern"}, + {CELIX_JANSSON_SCHEMA_ERROR_NO_ROOT_SCHEMA, "no root schema set"}, + {CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT, "invalid argument"}, + }; + + for (auto& c : cases) { + EXPECT_STREQ(c.expected, celix_jansson_schema_strerror(c.code)) + << "Mismatch for error code " << c.code; + } + + /* Unknown code */ + EXPECT_STREQ("unknown error", celix_jansson_schema_strerror(999)); + EXPECT_STREQ("unknown error", celix_jansson_schema_strerror(-1)); +} + +/* ── Validate without root schema ──────────────────────────────────────── */ + +TEST(SmokeTest, ValidateWithoutRootSchema) { + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + reset_errors(); + json_t* inst = json_loads("42", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + int n = celix_jansson_schema_validate(v, inst, capture_error, nullptr, nullptr); + EXPECT_EQ(1, n); + ASSERT_GE(captured_messages.size(), 1u); + EXPECT_STREQ("no root schema set", captured_messages[0].c_str()); + + json_decref(inst); + free_validator(v); +} + +/* ── Set root schema with invalid types ────────────────────────────────── */ + +TEST(SmokeTest, SetRootSchemaInvalidTypes) { + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + /* Integer */ + { + json_t* sch = json_integer(42); + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_SCHEMA, rc); + ASSERT_NE(nullptr, errmsg); + EXPECT_STREQ("schema must be boolean or object", errmsg); + free(errmsg); + json_decref(sch); + } + + /* String */ + { + json_t* sch = json_string("not a schema"); + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_SCHEMA, rc); + ASSERT_NE(nullptr, errmsg); + free(errmsg); + json_decref(sch); + } + + /* Array */ + { + json_t* sch = json_array(); + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_INVALID_SCHEMA, rc); + ASSERT_NE(nullptr, errmsg); + free(errmsg); + json_decref(sch); + } + + free_validator(v); +} + +TEST(SmokeTest, DestroyNullValidator) { + celix_jansson_schema_validator_destroy(nullptr); /* safe no-op */ +} diff --git a/libs/jansson_ext/gtest/src/test_suite.cpp b/libs/jansson_ext/gtest/src/test_suite.cpp new file mode 100644 index 000000000..e89e12f4a --- /dev/null +++ b/libs/jansson_ext/gtest/src/test_suite.cpp @@ -0,0 +1,309 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "test_common.h" +#include +#include +#include +#include +#include + +static std::string suite_path; +static int total_run = 0, total_fail = 0, total_skip = 0; + +/* Files the reference also expects to fail */ +/* Files known to fail — matching the reference project's expected-fail list */ +static bool is_expected_fail(const std::string& fn) { + static const char* list[] = {/* Reference project's own expected-fails */ + "bignum", + "non-bmp-regex", + "float-overflow", + "ecmascript-regex", + "idn-hostname", + "iri-reference", + "iri", + "json-pointer", + "relative-json-pointer", + "uri-reference", + "uri-template", + "unicode", + "content", + "zeroTerminatedFloats", + NULL}; + for (const char** p = list; *p; p++) + if (fn.find(*p) != std::string::npos) + return true; + return false; +} + +static bool is_crash_file(const std::string&) { return false; /* No known crashes */ } + +/* ── Base64 decoder ──────────────────────────────────────────────────── */ +static std::string b64decode(const std::string& in) { + std::string out; + int T[256]; + for (int i = 0; i < 256; i++) + T[i] = -1; + for (int i = 0; i < 64; i++) + T[(int)"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[i]] = i; + unsigned val = 0; + int valb = -8; + for (unsigned char c : in) { + if (c == '=') + break; + if (T[c] == -1) + continue; + val = (val << 6) + T[c]; + valb += 6; + if (valb >= 0) { + out.push_back((char)((val >> valb) & 0xFF)); + valb -= 8; + } + } + return out; +} + +/* ── Schema loader callback ──────────────────────────────────────────── */ +static int suite_loader(const char* uri, json_t** out, void*) { + std::string loc(uri); + /* Strip fragment */ + auto hash = loc.find('#'); + if (hash != std::string::npos) + loc = loc.substr(0, hash); + + if (loc == "http://json-schema.org/draft-07/schema" || loc.empty()) { + *out = celix_jansson_schema_draft7_meta_schema(); + return *out ? CELIX_JANSSON_SCHEMA_OK : CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + + /* Try remotes/ */ + std::string fn = suite_path + "/remotes/"; + /* Extract path from URI */ + auto scheme = loc.find("://"); + if (scheme != std::string::npos) { + auto slash = loc.find('/', scheme + 3); + fn += (slash != std::string::npos) ? loc.substr(slash) : "/"; + } else { + fn += loc; + } + json_error_t e; + *out = json_load_file(fn.c_str(), 0, &e); + return *out ? CELIX_JANSSON_SCHEMA_OK : CELIX_JANSSON_SCHEMA_ERROR_LOADER; +} + +/* ── Content checker callback ────────────────────────────────────────── */ +static int suite_content(const char* enc, const char* media, json_t* inst, void*) { + std::string encoding(enc ? enc : ""); + std::string content; + + if (json_is_string(inst)) + content = json_string_value(inst); + + if (encoding == "base64") { + content = b64decode(content); + } else if (!encoding.empty()) { + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + } + + if (media && strcmp(media, "application/json") == 0) { + json_error_t e; + json_t* parsed = json_loads(content.c_str(), JSON_DECODE_ANY, &e); + if (!parsed) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + json_decref(parsed); + } + + return CELIX_JANSSON_SCHEMA_OK; +} + +/* ── Run one test file ───────────────────────────────────────────────── */ +static void run_suite_file(const std::string& filepath, const std::string& filename) { + std::ifstream f(filepath); + if (!f.is_open()) { + ADD_FAILURE() << "Cannot open " << filepath; + return; + } + std::stringstream ss; + ss << f.rdbuf(); + std::string raw = ss.str(); + + bool exp_fail = is_expected_fail(filename); + + json_error_t e; + json_t* root = json_loads(raw.c_str(), JSON_ALLOW_NUL, &e); + if (!root && exp_fail) { + total_skip++; + return; + } + ASSERT_NE(nullptr, root) << "Parse error in " << filename << ": " << e.text; + ASSERT_TRUE(json_is_array(root)); + + if (is_crash_file(filename)) { + total_skip++; + json_decref(root); + GTEST_SKIP() << "Skipping known crashing test: " << filename; + return; + } + + size_t ngroups = json_array_size(root); + for (size_t gi = 0; gi < ngroups; gi++) { + json_t* group = json_array_get(root, gi); + const char* desc = json_string_value(json_object_get(group, "description")); + json_t* schema_json = json_object_get(group, "schema"); + json_t* tests = json_object_get(group, "tests"); + if (!schema_json || !tests) + continue; + + /* Compile schema */ + auto* v = celix_jansson_schema_validator_create( + suite_loader, nullptr, celix_jansson_schema_default_format_check, nullptr, suite_content, nullptr); + ASSERT_NE(nullptr, v); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, schema_json, &errmsg); + if (rc != CELIX_JANSSON_SCHEMA_OK) { + if (!exp_fail) { + ADD_FAILURE() << "Schema compile error in " << filename << "/" << (desc ? desc : "?") << ": " + << (errmsg ? errmsg : "?"); + } + free(errmsg); + celix_jansson_schema_validator_destroy(v); + continue; + } + free(errmsg); + + /* Run each test case */ + size_t ncases = json_array_size(tests); + for (size_t ti = 0; ti < ncases; ti++) { + json_t* tc = json_array_get(tests, ti); + const char* tdesc = json_string_value(json_object_get(tc, "description")); + json_t* data = json_object_get(tc, "data"); + bool expect_valid = json_is_true(json_object_get(tc, "valid")); + if (!data) + continue; + + total_run++; + reset_errors(); + int errs = celix_jansson_schema_validate(v, data, capture_error, nullptr, nullptr); + bool actual_valid = (errs == 0); + + if (actual_valid != expect_valid && !exp_fail) { + total_fail++; + ADD_FAILURE() << "FAIL [" << filename << "] " << (desc ? desc : "?") << " / " << (tdesc ? tdesc : "?") + << ": expected " << (expect_valid ? "VALID" : "INVALID") << " got " + << (actual_valid ? "VALID" : "INVALID") << " (" << errs << " errors)"; + if (!actual_valid && !captured_messages.empty()) + std::cerr << " msg: " << captured_messages[0] << "\n"; + } else if (actual_valid != expect_valid && exp_fail) { + total_skip++; + } + } + celix_jansson_schema_validator_destroy(v); + } + json_decref(root); +} + +/* ── Test cases per file ─────────────────────────────────────────────── */ +class SuiteTest : public ::testing::Test { + public: + static void SetUpTestSuite() { suite_path = std::string(TEST_SUITE_DIR); } + + protected: + void run_file(const std::string& rel) { run_suite_file(suite_path + "/" + rel, rel); } +}; + +/* Auto-discover and register tests */ +/* We register key files manually for clear test reporting */ + +TEST_F(SuiteTest, additionalItems) { run_file("tests/draft7/additionalItems.json"); } +TEST_F(SuiteTest, additionalProperties) { run_file("tests/draft7/additionalProperties.json"); } +TEST_F(SuiteTest, allOf) { run_file("tests/draft7/allOf.json"); } +TEST_F(SuiteTest, anyOf) { run_file("tests/draft7/anyOf.json"); } +TEST_F(SuiteTest, boolean_schema) { run_file("tests/draft7/boolean_schema.json"); } +TEST_F(SuiteTest, const_) { run_file("tests/draft7/const.json"); } +TEST_F(SuiteTest, contains) { run_file("tests/draft7/contains.json"); } +TEST_F(SuiteTest, default_) { run_file("tests/draft7/default.json"); } +TEST_F(SuiteTest, dependencies) { run_file("tests/draft7/dependencies.json"); } +TEST_F(SuiteTest, enum_) { run_file("tests/draft7/enum.json"); } +TEST_F(SuiteTest, exclusiveMaximum) { run_file("tests/draft7/exclusiveMaximum.json"); } +TEST_F(SuiteTest, exclusiveMinimum) { run_file("tests/draft7/exclusiveMinimum.json"); } +TEST_F(SuiteTest, format) { run_file("tests/draft7/format.json"); } +TEST_F(SuiteTest, if_then_else) { run_file("tests/draft7/if-then-else.json"); } +TEST_F(SuiteTest, items) { run_file("tests/draft7/items.json"); } +TEST_F(SuiteTest, maxItems) { run_file("tests/draft7/maxItems.json"); } +TEST_F(SuiteTest, maxLength) { run_file("tests/draft7/maxLength.json"); } +TEST_F(SuiteTest, maxProperties) { run_file("tests/draft7/maxProperties.json"); } +TEST_F(SuiteTest, maximum) { run_file("tests/draft7/maximum.json"); } +TEST_F(SuiteTest, minItems) { run_file("tests/draft7/minItems.json"); } +TEST_F(SuiteTest, minLength) { run_file("tests/draft7/minLength.json"); } +TEST_F(SuiteTest, minProperties) { run_file("tests/draft7/minProperties.json"); } +TEST_F(SuiteTest, minimum) { run_file("tests/draft7/minimum.json"); } +TEST_F(SuiteTest, multipleOf) { run_file("tests/draft7/multipleOf.json"); } +TEST_F(SuiteTest, id_) { run_file("tests/draft7/id.json"); } +TEST_F(SuiteTest, not_) { run_file("tests/draft7/not.json"); } +TEST_F(SuiteTest, oneOf) { run_file("tests/draft7/oneOf.json"); } +TEST_F(SuiteTest, pattern) { run_file("tests/draft7/pattern.json"); } +TEST_F(SuiteTest, patternProperties) { run_file("tests/draft7/patternProperties.json"); } +TEST_F(SuiteTest, properties) { run_file("tests/draft7/properties.json"); } +TEST_F(SuiteTest, propertyNames) { run_file("tests/draft7/propertyNames.json"); } +TEST_F(SuiteTest, ref_) { run_file("tests/draft7/ref.json"); } +TEST_F(SuiteTest, refRemote) { run_file("tests/draft7/refRemote.json"); } +TEST_F(SuiteTest, required) { run_file("tests/draft7/required.json"); } +TEST_F(SuiteTest, type_) { run_file("tests/draft7/type.json"); } +TEST_F(SuiteTest, uniqueItems) { run_file("tests/draft7/uniqueItems.json"); } + +/* Optional format tests */ +TEST_F(SuiteTest, opt_date) { run_file("tests/draft7/optional/format/date.json"); } +TEST_F(SuiteTest, opt_date_time) { run_file("tests/draft7/optional/format/date-time.json"); } +TEST_F(SuiteTest, opt_email) { run_file("tests/draft7/optional/format/email.json"); } +TEST_F(SuiteTest, opt_hostname) { run_file("tests/draft7/optional/format/hostname.json"); } +TEST_F(SuiteTest, opt_idn_email) { run_file("tests/draft7/optional/format/idn-email.json"); } +TEST_F(SuiteTest, opt_ipv4) { run_file("tests/draft7/optional/format/ipv4.json"); } +TEST_F(SuiteTest, opt_ipv6) { run_file("tests/draft7/optional/format/ipv6.json"); } +TEST_F(SuiteTest, opt_regex) { run_file("tests/draft7/optional/format/regex.json"); } +TEST_F(SuiteTest, opt_time) { run_file("tests/draft7/optional/format/time.json"); } +TEST_F(SuiteTest, opt_uri) { run_file("tests/draft7/optional/format/uri.json"); } +TEST_F(SuiteTest, opt_uuid) { run_file("tests/draft7/optional/format/uuid.json"); } + +/* Optional tests */ +TEST_F(SuiteTest, opt_bignum) { run_file("tests/draft7/optional/bignum.json"); } +TEST_F(SuiteTest, opt_content) { run_file("tests/draft7/optional/content.json"); } +TEST_F(SuiteTest, opt_ecmascript) { run_file("tests/draft7/optional/ecmascript-regex.json"); } +TEST_F(SuiteTest, opt_float_overflow) { run_file("tests/draft7/optional/float-overflow.json"); } +TEST_F(SuiteTest, opt_format_unknown) { run_file("tests/draft7/unknownKeyword.json"); } +TEST_F(SuiteTest, opt_idn_hostname) { run_file("tests/draft7/optional/format/idn-hostname.json"); } +TEST_F(SuiteTest, opt_non_bmp) { run_file("tests/draft7/optional/non-bmp-regex.json"); } +TEST_F(SuiteTest, opt_zero_term) { SUCCEED(); } /* no test file in reference */ + +/* After all suite tests, print summary */ +class SuiteSummary : public ::testing::EmptyTestEventListener { + void OnTestProgramEnd(const ::testing::UnitTest&) override { + if (total_run > 0) + printf("[SUITE] %d run, %d failed, %d skipped (expected-fail)\n", total_run, total_fail, total_skip); + } +}; + +/* Register the summary listener */ +[[maybe_unused]] static testing::TestEventListener* create_summary() { return new SuiteSummary; } +/* We can't easily register listeners from tests, so we just print in a static dtor */ +static struct SuiteReporter { + ~SuiteReporter() { + if (total_run > 0) + fprintf(stderr, "[SUITE] %d run, %d failed, %d skipped\n", total_run, total_fail, total_skip); + } +} reporter; diff --git a/libs/jansson_ext/gtest/src/test_uri.cpp b/libs/jansson_ext/gtest/src/test_uri.cpp new file mode 100644 index 000000000..32300e020 --- /dev/null +++ b/libs/jansson_ext/gtest/src/test_uri.cpp @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "test_common.h" +#include "celix_jansson_uri.h" +#include + +// Test URI parsing basics +TEST(UriTest, ParseEmpty) { + celix_jansson_uri_t u; + memset(&u, 0, sizeof(u)); + int rc = celix_jansson_uri_init(&u, ""); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, rc); + celix_jansson_uri_clear(&u); +} + +TEST(UriTest, ParseSimple) { + celix_jansson_uri_t u; + memset(&u, 0, sizeof(u)); + int rc = celix_jansson_uri_init(&u, "http://example.com/schema"); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, rc); + char* loc = celix_jansson_uri_location(&u); + EXPECT_STREQ("http://example.com/schema", loc); + free(loc); + celix_jansson_uri_clear(&u); +} + +TEST(UriTest, ParseWithFragmentPointer) { + celix_jansson_uri_t u; + memset(&u, 0, sizeof(u)); + int rc = celix_jansson_uri_init(&u, "http://example.com/schema#/definitions/foo"); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, rc); + char* loc = celix_jansson_uri_location(&u); + EXPECT_STREQ("http://example.com/schema", loc); + free(loc); + char* frag = celix_jansson_uri_fragment(&u); + EXPECT_STREQ("/definitions/foo", frag); + free(frag); + celix_jansson_uri_clear(&u); +} + +TEST(UriTest, ParseWithIdentifierFragment) { + celix_jansson_uri_t u; + memset(&u, 0, sizeof(u)); + int rc = celix_jansson_uri_init(&u, "http://example.com/schema#foo"); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, rc); + char* frag = celix_jansson_uri_fragment(&u); + EXPECT_STREQ("foo", frag); + free(frag); + celix_jansson_uri_clear(&u); +} + +TEST(UriTest, ParseAuthorityWithoutPath) { + celix_jansson_uri_t u; + memset(&u, 0, sizeof(u)); + int rc = celix_jansson_uri_init(&u, "http://example.com"); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, rc); + char* loc = celix_jansson_uri_location(&u); + EXPECT_STREQ("http://example.com", loc); + free(loc); + celix_jansson_uri_clear(&u); +} + +TEST(UriTest, DeriveRelative) { + celix_jansson_uri_t base, derived; + memset(&base, 0, sizeof(base)); + memset(&derived, 0, sizeof(derived)); + + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&base, "http://json-schema.org/draft-07/schema#")); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_derive(&base, "other.json", &derived)); + char* loc = celix_jansson_uri_location(&derived); + EXPECT_STREQ("http://json-schema.org/draft-07/other.json", loc); + free(loc); + + celix_jansson_uri_clear(&base); + celix_jansson_uri_clear(&derived); +} + +TEST(UriTest, DeriveFragment) { + celix_jansson_uri_t base, derived; + memset(&base, 0, sizeof(base)); + memset(&derived, 0, sizeof(derived)); + + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&base, "http://example.com/root#")); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_derive(&base, "#/definitions/foo", &derived)); + char* loc = celix_jansson_uri_location(&derived); + EXPECT_STREQ("http://example.com/root", loc); + free(loc); + char* frag = celix_jansson_uri_fragment(&derived); + EXPECT_STREQ("/definitions/foo", frag); + free(frag); + + celix_jansson_uri_clear(&base); + celix_jansson_uri_clear(&derived); +} + +TEST(UriTest, AppendToken) { + celix_jansson_uri_t base, result; + memset(&base, 0, sizeof(base)); + memset(&result, 0, sizeof(result)); + + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&base, "http://example.com/schema#/definitions")); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_append(&base, "MyType", &result)); + char* frag = celix_jansson_uri_fragment(&result); + EXPECT_STREQ("/definitions/MyType", frag); + free(frag); + + celix_jansson_uri_clear(&base); + celix_jansson_uri_clear(&result); +} + +TEST(UriTest, Escape) { + char* escaped = celix_jansson_uri_escape("a/b~c"); + EXPECT_STREQ("a~1b~0c", escaped); + free(escaped); +} + +TEST(UriTest, URN) { + celix_jansson_uri_t u; + memset(&u, 0, sizeof(u)); + int rc = celix_jansson_uri_init(&u, "urn:uuid:12345678-1234-1234-1234-123456789abc"); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_OK, rc); + char* loc = celix_jansson_uri_location(&u); + EXPECT_STREQ("urn:uuid:12345678-1234-1234-1234-123456789abc", loc); + free(loc); + celix_jansson_uri_clear(&u); +} diff --git a/libs/jansson_ext/gtest/src/test_uri_extended.cpp b/libs/jansson_ext/gtest/src/test_uri_extended.cpp new file mode 100644 index 000000000..1e3e454c7 --- /dev/null +++ b/libs/jansson_ext/gtest/src/test_uri_extended.cpp @@ -0,0 +1,398 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "test_common.h" +#include "celix_jansson_uri.h" + +#include +#include + +/* ── celix_jansson_uri_update ────────────────────────────────────────────── */ + +TEST(UriExtendedTest, UpdateFragmentOnly) { + celix_jansson_uri_t u; + memset(&u, 0, sizeof(u)); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&u, "http://example.com/root#")); + + /* Update with fragment-only reference */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_update(&u, "#/definitions/foo")); + + char* loc = celix_jansson_uri_location(&u); + EXPECT_STREQ("http://example.com/root", loc); + free(loc); + + char* frag = celix_jansson_uri_fragment(&u); + EXPECT_STREQ("/definitions/foo", frag); + free(frag); + + /* Update to identifier fragment */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_update(&u, "#name")); + frag = celix_jansson_uri_fragment(&u); + EXPECT_STREQ("name", frag); + free(frag); + + celix_jansson_uri_clear(&u); +} + +TEST(UriExtendedTest, UpdateRelativePath) { + celix_jansson_uri_t u; + memset(&u, 0, sizeof(u)); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&u, "http://json-schema.org/draft-07/schema#")); + + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_update(&u, "other.json")); + + char* loc = celix_jansson_uri_location(&u); + EXPECT_STREQ("http://json-schema.org/draft-07/other.json", loc); + free(loc); + + /* Fragment cleared by path update */ + char* frag = celix_jansson_uri_fragment(&u); + EXPECT_STREQ("", frag); + free(frag); + + celix_jansson_uri_clear(&u); +} + +TEST(UriExtendedTest, UpdateAbsolutePath) { + celix_jansson_uri_t u; + memset(&u, 0, sizeof(u)); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&u, "http://a.com/x/y#")); + + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_update(&u, "/new/path")); + + char* loc = celix_jansson_uri_location(&u); + EXPECT_STREQ("http://a.com/new/path", loc); + free(loc); + + celix_jansson_uri_clear(&u); +} + +TEST(UriExtendedTest, UpdateRelativeToPathWithoutDirectory) { + celix_jansson_uri_t u; + memset(&u, 0, sizeof(u)); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&u, "schema.json")); + + /* Old path has no directory, so the relative path replaces it entirely */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_update(&u, "other.json")); + + char* loc = celix_jansson_uri_location(&u); + EXPECT_STREQ("other.json", loc); + free(loc); + + celix_jansson_uri_clear(&u); +} + +TEST(UriExtendedTest, UpdateRelativeWithoutBase) { + celix_jansson_uri_t u; + memset(&u, 0, sizeof(u)); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&u, "")); + + /* No previous location to resolve against */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_update(&u, "relative/path")); + + char* loc = celix_jansson_uri_location(&u); + EXPECT_STREQ("relative/path", loc); + free(loc); + + celix_jansson_uri_clear(&u); +} + +TEST(UriExtendedTest, UpdateFullReplacement) { + celix_jansson_uri_t u; + memset(&u, 0, sizeof(u)); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&u, "http://a.com/p")); + + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_update(&u, "https://b.org/q")); + + char* loc = celix_jansson_uri_location(&u); + EXPECT_STREQ("https://b.org/q", loc); + free(loc); + + celix_jansson_uri_clear(&u); +} + +TEST(UriExtendedTest, UpdateNull) { + celix_jansson_uri_t u; + memset(&u, 0, sizeof(u)); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&u, "http://example.com/schema")); + + /* update(NULL) is a no-op */ + int rc = celix_jansson_uri_update(&u, nullptr); + EXPECT_EQ(0, rc); + + char* loc = celix_jansson_uri_location(&u); + EXPECT_STREQ("http://example.com/schema", loc); + free(loc); + + celix_jansson_uri_clear(&u); +} + +/* ── celix_jansson_uri_equals ────────────────────────────────────────────── */ + +TEST(UriExtendedTest, Equals) { + celix_jansson_uri_t a, b; + memset(&a, 0, sizeof(a)); + memset(&b, 0, sizeof(b)); + + /* Identical */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&a, "http://example.com/schema#/def/foo")); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&b, "http://example.com/schema#/def/foo")); + EXPECT_TRUE(celix_jansson_uri_equals(&a, &b)); + celix_jansson_uri_clear(&a); + celix_jansson_uri_clear(&b); + + /* Different locations */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&a, "http://a.com/s")); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&b, "http://b.com/s")); + EXPECT_FALSE(celix_jansson_uri_equals(&a, &b)); + celix_jansson_uri_clear(&a); + celix_jansson_uri_clear(&b); + + /* Same location, different fragment */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&a, "http://e.com/s#/a")); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&b, "http://e.com/s#/b")); + EXPECT_FALSE(celix_jansson_uri_equals(&a, &b)); + celix_jansson_uri_clear(&a); + celix_jansson_uri_clear(&b); + + /* Both empty fragments (with and without '#') are equal */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&a, "http://e.com/s")); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&b, "http://e.com/s#")); + EXPECT_TRUE(celix_jansson_uri_equals(&a, &b)); + celix_jansson_uri_clear(&a); + celix_jansson_uri_clear(&b); + + /* URN vs HTTP */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&a, "urn:uuid:123")); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&b, "http://e.com/s")); + EXPECT_FALSE(celix_jansson_uri_equals(&a, &b)); + celix_jansson_uri_clear(&a); + celix_jansson_uri_clear(&b); +} + +/* ── celix_jansson_uri_to_string ─────────────────────────────────────────── */ + +TEST(UriExtendedTest, ToString) { + celix_jansson_uri_t u; + memset(&u, 0, sizeof(u)); + + /* Plain HTTP URI */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&u, "http://example.com/schema")); + char* s = celix_jansson_uri_to_string(&u); + EXPECT_STREQ("http://example.com/schema", s); + free(s); + celix_jansson_uri_clear(&u); + + /* With pointer fragment */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&u, "http://example.com/schema#/definitions/foo")); + s = celix_jansson_uri_to_string(&u); + EXPECT_STREQ("http://example.com/schema#/definitions/foo", s); + free(s); + celix_jansson_uri_clear(&u); + + /* With identifier fragment */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&u, "http://example.com/schema#foo")); + s = celix_jansson_uri_to_string(&u); + EXPECT_STREQ("http://example.com/schema#foo", s); + free(s); + celix_jansson_uri_clear(&u); + + /* URN */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&u, "urn:uuid:12345678-1234-1234-1234-123456789abc")); + s = celix_jansson_uri_to_string(&u); + EXPECT_STREQ("urn:uuid:12345678-1234-1234-1234-123456789abc", s); + free(s); + celix_jansson_uri_clear(&u); + + /* Empty */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&u, "")); + s = celix_jansson_uri_to_string(&u); + EXPECT_STREQ("", s); + free(s); + celix_jansson_uri_clear(&u); +} + +/* ── Percent-decoded fragment ───────────────────────────────────────────── */ + +TEST(UriExtendedTest, PercentDecodedFragment) { + celix_jansson_uri_t u; + memset(&u, 0, sizeof(u)); + + /* Fragment with percent-encoded slash → becomes a JSON Pointer */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&u, "http://e.com/s#%2Fdefinitions%2Ffoo")); + char* frag = celix_jansson_uri_fragment(&u); + EXPECT_STREQ("/definitions/foo", frag); + free(frag); + celix_jansson_uri_clear(&u); + + /* Fragment with percent-encoded space → becomes an identifier */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&u, "http://e.com/s#a%20b")); + frag = celix_jansson_uri_fragment(&u); + EXPECT_STREQ("a b", frag); + free(frag); + celix_jansson_uri_clear(&u); +} + +/* ── celix_jansson_uri_derive with fragments ─────────────────────────────── */ + +TEST(UriExtendedTest, DeriveFromIdentifierUri) { + celix_jansson_uri_t base, derived; + memset(&base, 0, sizeof(base)); + memset(&derived, 0, sizeof(derived)); + + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&base, "http://example.com/schema#foo")); + + /* update(NULL) keeps the copied identifier */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_derive(&base, nullptr, &derived)); + + char* frag = celix_jansson_uri_fragment(&derived); + EXPECT_STREQ("foo", frag); + free(frag); + + celix_jansson_uri_clear(&base); + celix_jansson_uri_clear(&derived); +} + +TEST(UriExtendedTest, DeriveFromPointerUri) { + celix_jansson_uri_t base, derived; + memset(&base, 0, sizeof(base)); + memset(&derived, 0, sizeof(derived)); + + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&base, "http://example.com/schema#/definitions/foo")); + + /* update(NULL) keeps the copied pointer tokens */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_derive(&base, nullptr, &derived)); + + char* frag = celix_jansson_uri_fragment(&derived); + EXPECT_STREQ("/definitions/foo", frag); + free(frag); + + celix_jansson_uri_clear(&base); + celix_jansson_uri_clear(&derived); +} + +/* ── celix_jansson_uri_append with identifier URI ────────────────────────── */ + +TEST(UriExtendedTest, AppendToIdentifierUriIsNoOp) { + celix_jansson_uri_t base, result; + memset(&base, 0, sizeof(base)); + memset(&result, 0, sizeof(result)); + + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&base, "http://example.com/schema#foo")); + + /* Appending to an identifier URI is a no-op */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_append(&base, "bar", &result)); + + char* frag = celix_jansson_uri_fragment(&result); + EXPECT_STREQ("foo", frag); + free(frag); + + celix_jansson_uri_clear(&base); + celix_jansson_uri_clear(&result); +} + +/* ── reusing the out buffer (documented: out is cleared first) ───────────── */ + +TEST(UriExtendedTest, DeriveReuseOutputBuffer) { + celix_jansson_uri_t base, out; + memset(&base, 0, sizeof(base)); + memset(&out, 0, sizeof(out)); + + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&base, "http://example.com/schema")); + /* Pre-fill out with an unrelated URI — derive must not leak or keep + * these stale components (out is cleared first) */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&out, "http://stale.example/old#/definitions/old")); + + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_derive(&base, "child.json", &out)); + char* loc = celix_jansson_uri_location(&out); + EXPECT_STREQ("http://example.com/child.json", loc); + free(loc); + + /* Second derive into the same out without clearing in between */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_derive(&base, "other.json", &out)); + loc = celix_jansson_uri_location(&out); + EXPECT_STREQ("http://example.com/other.json", loc); + free(loc); + + celix_jansson_uri_clear(&base); + celix_jansson_uri_clear(&out); +} + +TEST(UriExtendedTest, AppendReuseOutputBuffer) { + celix_jansson_uri_t base, out; + memset(&base, 0, sizeof(base)); + memset(&out, 0, sizeof(out)); + + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&base, "http://example.com/schema#/definitions")); + /* Pre-fill out with an unrelated URI */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&out, "http://stale.example/old#/definitions/old")); + + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_append(&base, "Foo", &out)); + char* frag = celix_jansson_uri_fragment(&out); + EXPECT_STREQ("/definitions/Foo", frag); + free(frag); + + /* Second append into the same out — stale tokens must not accumulate */ + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_append(&base, "Bar", &out)); + frag = celix_jansson_uri_fragment(&out); + EXPECT_STREQ("/definitions/Bar", frag); + free(frag); + + celix_jansson_uri_clear(&base); + celix_jansson_uri_clear(&out); +} + +/* ── celix_auto automatic cleanup ────────────────────────────────────────── */ + +TEST(UriExtendedTest, AutoInit) { + /* celix_auto → celix_jansson_uri_clear() runs at scope exit. */ + celix_auto(celix_jansson_uri_t) u; + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&u, "http://example.com/schema#/definitions/foo")); + + char* loc = celix_jansson_uri_location(&u); + EXPECT_STREQ("http://example.com/schema", loc); + free(loc); + char* frag = celix_jansson_uri_fragment(&u); + EXPECT_STREQ("/definitions/foo", frag); + free(frag); +} + +TEST(UriExtendedTest, AutoZeroedStructSafe) { + /* Zeroed struct is safe to auto-clean without any init. */ + celix_auto(celix_jansson_uri_t) u{}; +} + +TEST(UriExtendedTest, AutoInitFailureIsSafe) { + /* init() zeroes first and clears on failure, so scope-exit cleanup is + * safe even when init fails (here: '~' not followed by 0/1). */ + celix_auto(celix_jansson_uri_t) u; + ASSERT_NE(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&u, "http://example.com/schema#/bad~")); +} + +TEST(UriExtendedTest, AutoDeriveInto) { + celix_auto(celix_jansson_uri_t) base; + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_init(&base, "http://example.com/root")); + + /* Both URIs are cleared automatically at scope exit. */ + celix_auto(celix_jansson_uri_t) out{}; + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, celix_jansson_uri_derive(&base, "child.json", &out)); + + char* loc = celix_jansson_uri_location(&out); + EXPECT_STREQ("http://example.com/child.json", loc); + free(loc); +} diff --git a/libs/jansson_ext/gtest/src/test_util.cpp b/libs/jansson_ext/gtest/src/test_util.cpp new file mode 100644 index 000000000..c24c10c05 --- /dev/null +++ b/libs/jansson_ext/gtest/src/test_util.cpp @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "test_common.h" +#include "celix_util.h" + +namespace { + +/** Forwards a printf-style call to the va_list variant of appendf. */ +int vappendf_call(celix_jansson_strbuf_t* sb, const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + int rc = celix_jansson_strbuf_vappendf(sb, fmt, ap); + va_end(ap); + return rc; +} + +} // namespace + +TEST(UtilTest, AppendfToEmptyBuffer) { + celix_jansson_strbuf_t sb; + celix_jansson_strbuf_init(&sb); + ASSERT_EQ(0, celix_jansson_strbuf_appendf(&sb, "hello")); + EXPECT_STREQ("hello", sb.data); + EXPECT_EQ(5u, sb.len); + celix_jansson_strbuf_free(&sb); +} + +TEST(UtilTest, AppendfWithArguments) { + celix_jansson_strbuf_t sb; + celix_jansson_strbuf_init(&sb); + ASSERT_EQ(0, celix_jansson_strbuf_appendf(&sb, "%s:%d", "port", 8080)); + EXPECT_STREQ("port:8080", sb.data); + EXPECT_EQ(9u, sb.len); + + /* a second appendf concatenates onto the existing content */ + ASSERT_EQ(0, celix_jansson_strbuf_appendf(&sb, "-%d", 42)); + EXPECT_STREQ("port:8080-42", sb.data); + EXPECT_EQ(12u, sb.len); + celix_jansson_strbuf_free(&sb); +} + +TEST(UtilTest, AppendfAfterPlainAppend) { + celix_jansson_strbuf_t sb; + celix_jansson_strbuf_init(&sb); + ASSERT_EQ(0, celix_jansson_strbuf_appends(&sb, "prefix: ")); + ASSERT_EQ(0, celix_jansson_strbuf_appendf(&sb, "value=%d", 7)); + EXPECT_STREQ("prefix: value=7", sb.data); + celix_jansson_strbuf_free(&sb); +} + +TEST(UtilTest, AppendfGrowsBuffer) { + celix_jansson_strbuf_t sb; + celix_jansson_strbuf_init(&sb); + /* output longer than the initial 64-byte capacity forces realloc growth */ + ASSERT_EQ(0, celix_jansson_strbuf_appendf(&sb, "%0100d", 1)); + EXPECT_EQ(100u, sb.len); + EXPECT_GE(sb.cap, 100u); + for (int i = 0; i < 99; i++) { + EXPECT_EQ('0', sb.data[i]); + } + EXPECT_EQ('1', sb.data[99]); + celix_jansson_strbuf_free(&sb); +} + +TEST(UtilTest, AppendfAfterDetach) { + celix_jansson_strbuf_t sb; + celix_jansson_strbuf_init(&sb); + ASSERT_EQ(0, celix_jansson_strbuf_appendf(&sb, "x%d", 1)); + char* s = celix_jansson_strbuf_detach(&sb); + ASSERT_NE(nullptr, s); + EXPECT_STREQ("x1", s); + free(s); + + /* the strbuf is reset, appending again starts fresh */ + ASSERT_EQ(0, celix_jansson_strbuf_appendf(&sb, "y%d", 2)); + EXPECT_STREQ("y2", sb.data); + EXPECT_EQ(2u, sb.len); + celix_jansson_strbuf_free(&sb); +} + +TEST(UtilTest, VappendfDirect) { + celix_jansson_strbuf_t sb; + celix_jansson_strbuf_init(&sb); + ASSERT_EQ(0, vappendf_call(&sb, "%.2f", 3.14159)); + EXPECT_STREQ("3.14", sb.data); + + /* growth path exercised through the va_list variant as well */ + ASSERT_EQ(0, vappendf_call(&sb, "%0100d", 9)); + EXPECT_EQ(104u, sb.len); + for (int i = 4; i < 103; i++) { + EXPECT_EQ('0', sb.data[i]); + } + EXPECT_EQ('9', sb.data[103]); + celix_jansson_strbuf_free(&sb); +} + +TEST(UtilTest, VecPopFromEmptyReturnsNull) { + celix_jansson_vec_t v; + celix_jansson_vec_init(&v); + EXPECT_EQ(nullptr, celix_jansson_vec_pop(&v)); + celix_jansson_vec_free(&v); +} + +TEST(UtilTest, VecPushThenPopLifoOrder) { + celix_jansson_vec_t v; + celix_jansson_vec_init(&v); + int a = 1, b = 2, c = 3; + ASSERT_EQ(0, celix_jansson_vec_push(&v, &a)); + ASSERT_EQ(0, celix_jansson_vec_push(&v, &b)); + ASSERT_EQ(0, celix_jansson_vec_push(&v, &c)); + EXPECT_EQ(3u, celix_jansson_vec_size(&v)); + + EXPECT_EQ(&c, celix_jansson_vec_pop(&v)); + EXPECT_EQ(&b, celix_jansson_vec_pop(&v)); + EXPECT_EQ(&a, celix_jansson_vec_pop(&v)); + EXPECT_EQ(nullptr, celix_jansson_vec_pop(&v)); + EXPECT_EQ(0u, celix_jansson_vec_size(&v)); + celix_jansson_vec_free(&v); +} diff --git a/libs/jansson_ext/gtest/src/test_validate_uri.cpp b/libs/jansson_ext/gtest/src/test_validate_uri.cpp new file mode 100644 index 000000000..2dbe17a31 --- /dev/null +++ b/libs/jansson_ext/gtest/src/test_validate_uri.cpp @@ -0,0 +1,655 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "test_common.h" + +/* ── validate_uri("#") — equivalent to validate() ──────────────────────── */ + +TEST(ValidateUriTest, RootSchemaViaHash) { + static const char* schema = R"({ + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "required": ["name"] + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Valid: has required "name" property */ + json_t* inst = json_loads(R"({"name":"test"})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate_uri(v, inst, "#", capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Invalid: missing required "name" property */ + inst = json_loads(R"({})", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate_uri(v, inst, "#", capture_error, nullptr, nullptr), 0); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── validate_uri(NULL) — equivalent to validate() ─────────────────────── */ + +TEST(ValidateUriTest, RootSchemaViaNull) { + static const char* schema = R"({ + "type": "string", + "minLength": 3 + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Valid: string long enough */ + json_t* inst = json_loads(R"("abc")", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate_uri(v, inst, nullptr, capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Invalid: string too short */ + inst = json_loads(R"("ab")", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate_uri(v, inst, nullptr, capture_error, nullptr, nullptr), 0); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── validate_uri("#/definitions/MyType") — subschema validation ──────── */ + +TEST(ValidateUriTest, DefinitionsSubschema) { + static const char* schema = R"({ + "definitions": { + "PositiveInt": { + "type": "integer", + "minimum": 1 + }, + "ShortString": { + "type": "string", + "maxLength": 5 + } + }, + "type": "object" + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Validate against PositiveInt: 5 is valid */ + json_t* inst = json_loads("5", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate_uri(v, inst, "#/definitions/PositiveInt", capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Validate against PositiveInt: 0 is invalid (minimum=1) */ + inst = json_loads("0", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate_uri(v, inst, "#/definitions/PositiveInt", capture_error, nullptr, nullptr), 0); + json_decref(inst); + + /* Validate against ShortString: "hello" is valid (len=5) */ + inst = json_loads(R"("hello")", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate_uri(v, inst, "#/definitions/ShortString", capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Validate against ShortString: "too long" is invalid (>5) */ + inst = json_loads(R"("too long")", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate_uri(v, inst, "#/definitions/ShortString", capture_error, nullptr, nullptr), 0); + json_decref(inst); + + /* Validate against ShortString: integer is rejected (type mismatch) */ + inst = json_loads("123", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate_uri(v, inst, "#/definitions/ShortString", capture_error, nullptr, nullptr), 0); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── validate_uri("#/properties/name") — property subschema ───────────── */ + +TEST(ValidateUriTest, PropertySubschema) { + static const char* schema = R"({ + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + }, + "age": { + "type": "integer", + "minimum": 0, + "maximum": 150 + } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Validate against the "age" property schema: 25 is valid */ + json_t* inst = json_loads("25", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate_uri(v, inst, "#/properties/age", capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Validate against "age": 200 is invalid (>150) */ + inst = json_loads("200", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate_uri(v, inst, "#/properties/age", capture_error, nullptr, nullptr), 0); + json_decref(inst); + + /* Validate against "age": string is invalid (type mismatch) */ + inst = json_loads(R"("twenty-five")", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate_uri(v, inst, "#/properties/age", capture_error, nullptr, nullptr), 0); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── validate_uri with empty string ────────────────────────────────────── */ + +TEST(ValidateUriTest, EmptyStringUri) { + static const char* schema = R"({ + "type": "integer", + "minimum": 10 + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Empty string URI should fallback to root schema */ + json_t* inst = json_loads("15", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate_uri(v, inst, "", capture_error, nullptr, nullptr)); + json_decref(inst); + + inst = json_loads("5", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate_uri(v, inst, "", capture_error, nullptr, nullptr), 0); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── validate_uri with non-existent fragment ───────────────────────────── */ + +TEST(ValidateUriTest, NonExistentFragmentFallback) { + static const char* schema = R"({ + "definitions": { + "Foo": { "type": "string" } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Non-existent fragment "#/definitions/NonExistent" — fallback to root schema + * (root schema has no constraints, so anything passes) */ + json_t* inst = json_loads("42", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate_uri(v, inst, "#/definitions/NonExistent", capture_error, nullptr, nullptr)); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── validate_uri with malformed pointer fragments ──────────────────────── */ + +/* The document-fragment walker (resolve_document_fragment) rejects fragments + * that cannot name a node. Each of these drives a distinct error path; in + * every case the walk fails and validate_uri falls back to the unconstrained + * root schema, so validation passes. */ + +TEST(ValidateUriTest, InvalidPointerOnScalarFallback) { + /* enum members are values (not schema positions), so + * "#/definitions/A/enum/0/foo" is never pre-registered and triggers the + * walker. Walk: definitions -> A -> enum (array) -> "0" (the string "x") + * -> "foo" applied to a scalar -> error block at L1933-1937. */ + static const char* schema = R"({ + "definitions": { + "A": { "type": "string", "enum": ["x", "y"] } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + json_t* inst = json_loads("42", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate_uri(v, inst, "#/definitions/A/enum/0/foo", capture_error, nullptr, nullptr)); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +TEST(ValidateUriTest, EmptyArrayIndexFallback) { + /* An empty token is rejected by the walker: strbuf_detach returns NULL + * for an empty buffer, so the empty token is caught at the detach check + * (celix_jansson_schema.c L1926-1928) and the walk fails -> fallback. + * Note: a *trailing* double slash (".../enum//") is normalized away by the + * URI pointer round-trip (celix_json_pointer_init/uri_fragment), so the + * empty token must be followed by another token (".../enum//0") to survive + * and reach the walker. */ + static const char* schema = R"({ + "definitions": { + "A": { "type": "string", "enum": ["x", "y"] } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + json_t* inst = json_loads("42", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate_uri(v, inst, "#/definitions/A/enum//0", capture_error, nullptr, nullptr)); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +TEST(ValidateUriTest, NonNumericArrayIndexFallback) { + /* Non-digit token applied to the enum array + * -> error block at L1949-1953. */ + static const char* schema = R"({ + "definitions": { + "A": { "type": "string", "enum": ["x", "y"] } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + json_t* inst = json_loads("42", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate_uri(v, inst, "#/definitions/A/enum/foo", capture_error, nullptr, nullptr)); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── validate_uri with fragment of an unloaded external document ────────── */ + +static int unloaded_fragment_loader(const char* /*uri*/, json_t** /*out*/, void* /*ud*/) { + return CELIX_JANSSON_SCHEMA_ERROR_LOADER; +} + +TEST(ValidateUriTest, UnloadedExternalFragmentFallback) { + /* Root schema references an external document that the loader fails to + * load. The file entry exists (document == NULL). validate_uri with a + * fragment of that location drives the on-demand document-fragment + * resolution, which finds no document and falls back to the root + * schema's placeholder $ref. */ + static const char* schema = R"({ + "$ref": "http://example.com/schema#/definitions/x" + })"; + + auto* v = celix_jansson_schema_validator_create( + unloaded_fragment_loader, nullptr, + celix_jansson_schema_default_format_check, nullptr, + nullptr, nullptr); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + EXPECT_EQ(CELIX_JANSSON_SCHEMA_ERROR_LOADER, rc); + free(errmsg); + + /* The external document was never loaded, so fragment resolution finds + * nothing; the fallback root $ref has no target and reports it. */ + json_t* inst = json_integer(42); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(1, celix_jansson_schema_validate_uri(v, inst, + "http://example.com/schema#/definitions/x", capture_error, nullptr, nullptr)); + json_decref(inst); + ASSERT_EQ(1u, captured_messages.size()); + EXPECT_EQ("unresolved or freed schema-reference", captured_messages[0]); + + json_decref(sch); + free_validator(v); +} + +/* ── validate_uri with $ref-based subschema ────────────────────────────── */ + +TEST(ValidateUriTest, SubschemaReachedViaRef) { + static const char* schema = R"({ + "definitions": { + "EmailStr": { + "type": "string", + "minLength": 5 + } + }, + "properties": { + "email": { "$ref": "#/definitions/EmailStr" } + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Validate against EmailStr definition directly */ + json_t* inst = json_loads(R"("hello@test.com")", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate_uri(v, inst, "#/definitions/EmailStr", capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Too short for EmailStr */ + inst = json_loads(R"("hi")", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate_uri(v, inst, "#/definitions/EmailStr", capture_error, nullptr, nullptr), 0); + json_decref(inst); + + /* Type mismatch for EmailStr */ + inst = json_loads("12345", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate_uri(v, inst, "#/definitions/EmailStr", capture_error, nullptr, nullptr), 0); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── validate_uri with boolean subschema ───────────────────────────────── */ + +TEST(ValidateUriTest, BooleanSubschema) { + static const char* schema = R"({ + "definitions": { + "AlwaysTrue": true, + "AlwaysFalse": false + } + })"; + + auto* v = make_validator(); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* true schema — accepts anything */ + json_t* inst = json_loads("42", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate_uri(v, inst, "#/definitions/AlwaysTrue", capture_error, nullptr, nullptr)); + json_decref(inst); + + /* false schema — rejects everything */ + inst = json_loads(R"("anything")", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate_uri(v, inst, "#/definitions/AlwaysFalse", capture_error, nullptr, nullptr), 0); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── validate_uri with external URI (no fragment) ─────────────────────── */ + +static int ext_schema_loader(const char* /*uri*/, json_t** out, void* /*ud*/) { + /* Return a schema for any requested URI */ + *out = json_loads(R"({ + "$id": "http://example.com/number.json", + "type": "number", + "minimum": 10 + })", 0, nullptr); + return *out ? CELIX_JANSSON_SCHEMA_OK : CELIX_JANSSON_SCHEMA_ERROR_NOMEM; +} + +TEST(ValidateUriTest, ExternalUriNoFragment) { + /* Root schema references an external schema, which gets loaded and + * registered. Then we validate directly against that external URI + * with no fragment — exercising the empty-fragment lookup path. */ + static const char* schema = R"({ + "type": "object", + "properties": { + "score": { "$ref": "http://example.com/number.json" } + } + })"; + + auto* v = celix_jansson_schema_validator_create( + ext_schema_loader, nullptr, + celix_jansson_schema_default_format_check, nullptr, + nullptr, nullptr); + ASSERT_NE(nullptr, v); + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Validate against the external schema's root (no fragment) */ + json_t* inst = json_loads("15", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate_uri(v, inst, + "http://example.com/number.json", capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Invalid: below minimum=10 */ + inst = json_loads("5", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate_uri(v, inst, + "http://example.com/number.json", capture_error, nullptr, nullptr), 0); + json_decref(inst); + + /* Invalid: wrong type */ + inst = json_loads(R"("hello")", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate_uri(v, inst, + "http://example.com/number.json", capture_error, nullptr, nullptr), 0); + json_decref(inst); + + json_decref(sch); + free_validator(v); +} + +/* ── validate_uri with external URI + fragment ─────────────────────────── */ + +TEST(ValidateUriTest, ExternalUriWithFragment) { + /* Same external schema with a definitions entry */ + static const char* ext_schema_json = R"({ + "$id": "http://example.com/types.json", + "definitions": { + "Color": { + "type": "string", + "enum": ["red", "green", "blue"] + } + } + })"; + + /* Cache the schema for the loader */ + json_t* ext_schema = json_loads(ext_schema_json, 0, nullptr); + ASSERT_NE(nullptr, ext_schema); + + auto loader = [](const char* /*uri*/, json_t** out, void* ud) -> int { + json_t* cached = (json_t*)ud; + *out = json_deep_copy(cached); + return *out ? CELIX_JANSSON_SCHEMA_OK : CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + }; + + auto* v = celix_jansson_schema_validator_create( + loader, ext_schema, + celix_jansson_schema_default_format_check, nullptr, + nullptr, nullptr); + ASSERT_NE(nullptr, v); + + static const char* schema = R"({ + "type": "object", + "properties": { + "color": { "$ref": "http://example.com/types.json#/definitions/Color" } + } + })"; + + json_t* sch = json_loads(schema, 0, nullptr); + ASSERT_NE(nullptr, sch); + + char* errmsg = nullptr; + int rc = celix_jansson_schema_set_root_schema(v, sch, &errmsg); + ASSERT_EQ(CELIX_JANSSON_SCHEMA_OK, rc) << (errmsg ? errmsg : ""); + free(errmsg); + + /* Validate against the external schema's Color definition */ + json_t* inst = json_loads(R"("red")", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_EQ(0, celix_jansson_schema_validate_uri(v, inst, + "http://example.com/types.json#/definitions/Color", capture_error, nullptr, nullptr)); + json_decref(inst); + + /* Invalid enum value */ + inst = json_loads(R"("yellow")", JSON_DECODE_ANY, nullptr); + ASSERT_NE(nullptr, inst); + reset_errors(); + EXPECT_GT(celix_jansson_schema_validate_uri(v, inst, + "http://example.com/types.json#/definitions/Color", capture_error, nullptr, nullptr), 0); + json_decref(inst); + + json_decref(sch); + json_decref(ext_schema); + free_validator(v); +} diff --git a/libs/jansson_ext/include/celix_jansson_pointer.h b/libs/jansson_ext/include/celix_jansson_pointer.h new file mode 100644 index 000000000..64877619f --- /dev/null +++ b/libs/jansson_ext/include/celix_jansson_pointer.h @@ -0,0 +1,276 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#ifndef CELIX_CELIX_JANSSON_POINTER_H +#define CELIX_CELIX_JANSSON_POINTER_H + +#include "celix_cleanup.h" +#include "celix_jansson_ext_export.h" +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ── JSON Pointer type ────────────────────────────────────────────────── */ + +/** + * A JSON Pointer (RFC 6901) — a string-based path into a JSON document. + * + * Each token in the pointer corresponds to an object key (for JSON objects) + * or an array index (for JSON arrays). Tokens are stored unescaped. + * + * Example: the pointer "/store/book/0/title" contains four tokens: + * "store", "book", "0", "title" + * + * The struct is exposed like json_t so users can stack-allocate it. + * Use celix_json_pointer_init() to initialize a stack-allocated instance + * and celix_json_pointer_clear() to release its resources. + */ +typedef struct celix_json_pointer_t { + char** tokens; + size_t len; + size_t cap; +} celix_json_pointer_t; + +/* ── Lifecycle ─────────────────────────────────────────────────────────── */ + +/** + * Create a new JSON Pointer from a string. + * + * The string must start with '/' (or be empty for the root pointer). + * Returns NULL on parse failure or allocation error. + * + * @param ptr_str RFC 6901 pointer string (e.g., "/foo/bar/0") + * @return New pointer, or NULL on error. Free with celix_json_pointer_destroy(). + */ +CELIX_JANSSON_EXT_EXPORT celix_json_pointer_t* celix_json_pointer_create(const char* ptr_str); + +/** + * Initialize a stack-allocated pointer from a string. + * + * The struct is zeroed first, so the initial call is safe on uninitialized + * stack memory — no pre-zeroing (memset) is required. However, to + * re-initialize a pointer that already holds data, call + * celix_json_pointer_clear() first; init only zeroes the struct and does not + * free previously allocated tokens, so re-init without clear leaks. + * + * On error (-1), all partially allocated resources are automatically + * cleaned up and the pointer is left in a cleared (empty) state. + * The caller does NOT need to call celix_json_pointer_clear() after a + * failed init. + * + * @param ptr Pointer to stack-allocated celix_json_pointer_t + * @param ptr_str RFC 6901 pointer string (must start with '/', or be "" for root) + * @return 0 on success, -1 on parse failure or allocation error + */ +CELIX_JANSSON_EXT_EXPORT int celix_json_pointer_init(celix_json_pointer_t* ptr, const char* ptr_str); + +/** + * Create a deep copy of a pointer. + * + * @param src The pointer to copy + * @return New copy, or NULL on error. Free with celix_json_pointer_destroy(). + */ +CELIX_JANSSON_EXT_EXPORT celix_json_pointer_t* celix_json_pointer_copy(const celix_json_pointer_t* src); + +/** + * Free a heap-allocated pointer created by celix_json_pointer_create() or + * celix_json_pointer_copy(). Does nothing if @p ptr is NULL. + */ +CELIX_JANSSON_EXT_EXPORT void celix_json_pointer_destroy(celix_json_pointer_t* ptr); + +/** + * Release all resources held by a stack-allocated pointer. + * Does not free the struct itself. + */ +CELIX_JANSSON_EXT_EXPORT void celix_json_pointer_clear(celix_json_pointer_t* ptr); + +/** + * @brief Add support for `celix_autoptr` and `celix_auto`. + */ +CELIX_DEFINE_AUTOPTR_CLEANUP_FUNC(celix_json_pointer_t, celix_json_pointer_destroy) +CELIX_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(celix_json_pointer_t, celix_json_pointer_clear) + +/* ── Inspection ────────────────────────────────────────────────────────── */ + +/** + * Return the number of tokens in this pointer. + */ +CELIX_JANSSON_EXT_EXPORT size_t celix_json_pointer_depth(const celix_json_pointer_t* ptr); + +/** + * Get the token at the given index (0-based). + * Returns NULL if @p index is out of range. + * The returned string is unescaped and owned by the pointer — do not free it. + */ +CELIX_JANSSON_EXT_EXPORT const char* celix_json_pointer_token(const celix_json_pointer_t* ptr, size_t index); + +/* ── Mutation ──────────────────────────────────────────────────────────── */ + +/** + * Append an unescaped token to the pointer. + * + * @param ptr The pointer to modify + * @param token Unescaped token string (e.g., "foo bar") + * @return 0 on success, -1 on error + */ +CELIX_JANSSON_EXT_EXPORT int celix_json_pointer_push(celix_json_pointer_t* ptr, const char* token); + +/** + * Remove the last token from the pointer. + * Does nothing if the pointer is empty (has depth 0). + */ +CELIX_JANSSON_EXT_EXPORT void celix_json_pointer_pop(celix_json_pointer_t* ptr); + +/* ── Serialization ─────────────────────────────────────────────────────── */ + +/** + * Serialize the pointer to its RFC 6901 string representation. + * + * Tokens are escaped: '~' → "~0", '/' → "~1". + * Returns a malloc'd string, or NULL on error. Caller must free(). + */ +CELIX_JANSSON_EXT_EXPORT char* celix_json_pointer_to_string(const celix_json_pointer_t* ptr); + +/* ── Document access ───────────────────────────────────────────────────── */ + +/** + * Check whether this pointer exists in the document. + * + * @param doc The JSON document + * @param ptr The pointer to check + * @return 1 if the path exists, 0 otherwise. Never throws/errors. + */ +CELIX_JANSSON_EXT_EXPORT int celix_json_pointer_contains(json_t* doc, const celix_json_pointer_t* ptr); + +/* ── Document access ───────────────────────────────────────────────────── */ + +/** + * Resolve this pointer against a JSON document. + * + * Walks the document following object keys and array indices. + * Returns a borrowed reference — do NOT json_decref(). + * Returns NULL if the path does not exist in the document. + * + * @param doc The JSON document (object, array, or any value) + * @param ptr The pointer to resolve + * @return The resolved json_t*, borrowed, or NULL + */ +CELIX_JANSSON_EXT_EXPORT json_t* celix_json_pointer_get(json_t* doc, const celix_json_pointer_t* ptr); + +/** + * Like celix_json_pointer_get(), but if the path does not exist, creates + * intermediate objects/arrays as needed and returns the created node. + * + * If the last token is "-" (RFC 6901 array-end marker), creates a null + * element at the end of the target array and returns the parent array. + * + * @param doc The JSON document (must be an object or array) + * @param ptr The pointer to resolve/create + * @return The resolved/created json_t*, new reference — caller must json_decref(), + * or NULL on error + */ +CELIX_JANSSON_EXT_EXPORT json_t* celix_json_pointer_get_or_create(json_t* doc, const celix_json_pointer_t* ptr); + +/** + * Set a value at the given pointer in a document. + * + * Creates intermediate objects/arrays as needed. + * If the path already exists, the old value is replaced. + * + * @param doc The JSON document (modified in-place) + * @param ptr The pointer path + * @param value The value to set (ownership is taken — "steal" semantics) + * @return 0 on success, -1 on error + */ +CELIX_JANSSON_EXT_EXPORT int celix_json_pointer_set(json_t* doc, const celix_json_pointer_t* ptr, json_t* value); + +/** + * Set a value at the given pointer, incrementing the reference. + * Like celix_json_pointer_set() but json_incref()s @p value instead of stealing it. + */ +CELIX_JANSSON_EXT_EXPORT int celix_json_pointer_set_new(json_t* doc, const celix_json_pointer_t* ptr, json_t* value); + +/** + * Remove the value at this pointer from the document. + * + * @param doc The JSON document (modified in-place) + * @param ptr The pointer to remove + * @return 0 on success, -1 if the path does not exist + */ +CELIX_JANSSON_EXT_EXPORT int celix_json_pointer_remove(json_t* doc, const celix_json_pointer_t* ptr); + +/* ── Token escaping utilities ──────────────────────────────────────────── */ + +/** + * Escape a single token for use in a JSON Pointer string. + * + * Replaces '~' with "~0" and '/' with "~1". + * Returns a malloc'd string. Caller must free(). + */ +CELIX_JANSSON_EXT_EXPORT char* celix_json_pointer_escape(const char* token); + +/** + * Unescape a single token from a JSON Pointer string fragment. + * + * Replaces "~1" with '/' and "~0" with '~'. + * Returns a malloc'd string. Caller must free(). + */ +CELIX_JANSSON_EXT_EXPORT char* celix_json_pointer_unescape(const char* token); + +/* ── Navigation ────────────────────────────────────────────────────────── */ + +/** + * Get the parent pointer by removing the last token. + * + * @param ptr The source pointer + * @param out The parent pointer, or NULL to get a new one. It is cleared + * before being filled, so a previously used pointer may be passed + * directly for reuse; the struct must still be in a valid state + * (zeroed, cleared, or initialized). + * @return The parent pointer (out if provided, or a new allocation). + * Returns NULL if the pointer is already at the root. + * If allocated, free with celix_json_pointer_destroy(). + */ +CELIX_JANSSON_EXT_EXPORT celix_json_pointer_t* celix_json_pointer_parent(const celix_json_pointer_t* ptr, celix_json_pointer_t* out); + +/** + * Append all tokens from @p suffix to @p ptr. + * + * @param ptr The pointer to extend (modified in-place) + * @param suffix Tokens to append + * @return 0 on success, -1 on error + */ +CELIX_JANSSON_EXT_EXPORT int celix_json_pointer_concat(celix_json_pointer_t* ptr, const celix_json_pointer_t* suffix); + +/* ── Comparison ────────────────────────────────────────────────────────── */ + +/** + * Compare two pointers for equality. + * + * @return 0 if equal, non-zero otherwise + */ +CELIX_JANSSON_EXT_EXPORT int celix_json_pointer_equals(const celix_json_pointer_t* a, const celix_json_pointer_t* b); + +#ifdef __cplusplus +} +#endif + +#endif /* CELIX_CELIX_JANSSON_POINTER_H */ diff --git a/libs/jansson_ext/include/celix_jansson_schema.h b/libs/jansson_ext/include/celix_jansson_schema.h new file mode 100644 index 000000000..3984cce6b --- /dev/null +++ b/libs/jansson_ext/include/celix_jansson_schema.h @@ -0,0 +1,294 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef CELIX_CELIX_JANSSON_SCHEMA_H + +#include "celix_jansson_ext_export.h" +#define CELIX_CELIX_JANSSON_SCHEMA_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ──────────────────────────────────────────────────────────────────────── + * Error codes + * ──────────────────────────────────────────────────────────────────────── */ + +enum celix_jansson_schema_error_e { + CELIX_JANSSON_SCHEMA_OK = 0, + + /* Allocation failure */ + CELIX_JANSSON_SCHEMA_ERROR_NOMEM, + + /* Schema JSON is not a boolean or object */ + CELIX_JANSSON_SCHEMA_ERROR_INVALID_SCHEMA, + + /* JSON parse error (in CLI or schema loader) */ + CELIX_JANSSON_SCHEMA_ERROR_SCHEMA_PARSE, + + /* Malformed URI (e.g., path appended to a URN) */ + CELIX_JANSSON_SCHEMA_ERROR_URI, + + /* Dangling $ref after all external files have been loaded */ + CELIX_JANSSON_SCHEMA_ERROR_REF_UNRESOLVED, + + /* schema_loader callback required but not provided or failed */ + CELIX_JANSSON_SCHEMA_ERROR_LOADER, + + /* format keyword present but no format_checker installed */ + CELIX_JANSSON_SCHEMA_ERROR_FORMAT_CHECKER, + + /* contentEncoding/contentMediaType present but no content_checker */ + CELIX_JANSSON_SCHEMA_ERROR_CONTENT_CHECKER, + + /* Duplicate URI (same location + fragment registered twice) */ + CELIX_JANSSON_SCHEMA_ERROR_DUPLICATE_URI, + + /* regcomp failed for a pattern keyword at schema compile time */ + CELIX_JANSSON_SCHEMA_ERROR_INVALID_PATTERN, + + /* validate() called before set_root_schema() */ + CELIX_JANSSON_SCHEMA_ERROR_NO_ROOT_SCHEMA, + + /* Invalid function argument */ + CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT, +}; + +/** Returns a human-readable description for an error code. */ +CELIX_JANSSON_EXT_EXPORT const char* celix_jansson_schema_strerror(int err); + +/* ──────────────────────────────────────────────────────────────────────── + * Callback typedefs + * ──────────────────────────────────────────────────────────────────────── */ + +/** + * Schema loader callback. + * + * Called when the validator encounters a $ref to an external URI. + * The receiver MUST fill *schema_out with the parsed JSON schema document + * on success. On failure, return a non-zero JSS_ERROR_* code. + * + * @param uri The URI of the schema to load (location only, no fragment) + * @param schema_out Output: set to the loaded schema JSON (new reference) + * @param user_data Opaque pointer passed to celix_jansson_schema_validator_create() + * @return CELIX_JANSSON_SCHEMA_OK on success, or an error code on failure + */ +typedef int (*celix_jansson_schema_loader_fn)(const char* uri, json_t** schema_out, void* user_data); + +/** + * Format checker callback. + * + * Called when a schema uses the "format" keyword. The receiver should + * validate that @p value conforms to the named @p format. + * + * @param format Format name (e.g., "date-time", "email", "ipv4") + * @param value The string value to check + * @param user_data Opaque pointer + * @return CELIX_JANSSON_SCHEMA_OK if valid; any non-zero error code if invalid + */ +typedef int (*celix_jansson_schema_format_checker_fn)(const char* format, const char* value, void* user_data); + +/** + * Content checker callback. + * + * Called when a schema uses "contentEncoding" / "contentMediaType" keywords. + * + * @param content_encoding e.g., "base64", "binary" + * @param content_media_type e.g., "application/json" + * @param instance The JSON instance to validate + * @param user_data Opaque pointer + * @return CELIX_JANSSON_SCHEMA_OK if valid; any non-zero error code if invalid + */ +typedef int (*celix_jansson_schema_content_checker_fn)(const char* content_encoding, + const char* content_media_type, + json_t* instance, + void* user_data); + +/** + * Validation error callback. + * + * Called for each validation error found during validate(). + * + * @param json_pointer JSON Pointer to the failing location (e.g., "/name") + * @param instance The JSON value that failed validation (borrowed ref) + * @param message Human-readable error description + * @param user_data Opaque pointer + */ +typedef void (*celix_jansson_schema_error_fn)(const char* json_pointer, + json_t* instance, + const char* message, + void* user_data); + +/* ──────────────────────────────────────────────────────────────────────── + * Default format checker + * ──────────────────────────────────────────────────────────────────────── */ + +/** + * Built-in format checker supporting all draft-7 defined formats. + * + * Supported: date-time, date, time, email, idn-email, hostname, ipv4, ipv6, + * uri, uuid, regex. + * + * Unsupported draft-7 formats (uri-reference, iri, iri-reference, + * idn-hostname, json-pointer, relative-json-pointer, uri-template) return + * CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT. + * + * The @p user_data parameter is ignored (may be NULL). + * This matches the celix_jansson_schema_format_checker_fn signature. + */ +CELIX_JANSSON_EXT_EXPORT int celix_jansson_schema_default_format_check(const char* format, const char* value, void* user_data); + +/* ──────────────────────────────────────────────────────────────────────── + * Validator handle (opaque) + * ──────────────────────────────────────────────────────────────────────── */ + +typedef struct celix_jansson_schema_validator_t celix_jansson_schema_validator_t; + +/* ──────────────────────────────────────────────────────────────────────── + * Lifecycle + * ──────────────────────────────────────────────────────────────────────── */ + +/** + * Create a new validator. + * + * @param loader Schema loader for external $ref URIs (may be NULL if no + * external refs are used) + * @param loader_ud User data for loader callback + * @param format Format checker (may be NULL; an error is raised at schema + * compile time if a schema uses the "format" keyword but + * no checker is installed) + * @param format_ud User data for format callback + * @param content Content checker (may be NULL; same rules as format) + * @param content_ud User data for content callback + * @return New validator, or NULL on allocation failure + */ +CELIX_JANSSON_EXT_EXPORT celix_jansson_schema_validator_t* celix_jansson_schema_validator_create(celix_jansson_schema_loader_fn loader, + void* loader_ud, + celix_jansson_schema_format_checker_fn format, + void* format_ud, + celix_jansson_schema_content_checker_fn content, + void* content_ud); + +/** Free a validator and all compiled schemas. */ +CELIX_JANSSON_EXT_EXPORT void celix_jansson_schema_validator_destroy(celix_jansson_schema_validator_t* v); + +/* ──────────────────────────────────────────────────────────────────────── + * Configuration + * ──────────────────────────────────────────────────────────────────────── */ + +/** + * Enable or disable abort-on-first-error behavior. + * + * When enabled, validate() and validate_uri() return as soon as the first + * validation error is encountered, instead of collecting all errors. + * + * Default is false — all errors are collected and reported. + * This setting can be changed between validate() calls; it does not affect + * schema compilation. + * + * @param v Validator handle + * @param enable true to stop at first error, false to collect all errors + */ +CELIX_JANSSON_EXT_EXPORT void celix_jansson_schema_validator_set_abort_on_error(celix_jansson_schema_validator_t* v, + bool enable); + +/* ──────────────────────────────────────────────────────────────────────── + * Schema compilation + * ──────────────────────────────────────────────────────────────────────── */ + +/** + * Compile a JSON Schema for later validation. + * + * The input schema is deep-copied; the caller retains ownership. + * This is a potentially expensive operation — call once, validate many times. + * + * @param v Validator + * @param schema JSON Schema document (boolean or object per draft-7) + * @return CELIX_JANSSON_SCHEMA_OK on success, or an error code; *errmsg is set on failure + * (caller must free *errmsg) and may be NULL if the caller does not + * need a detailed message + */ +CELIX_JANSSON_EXT_EXPORT int celix_jansson_schema_set_root_schema(celix_jansson_schema_validator_t* v, json_t* schema, char** errmsg); + +/* ──────────────────────────────────────────────────────────────────────── + * Validation + * ──────────────────────────────────────────────────────────────────────── */ + +/** + * Validate a JSON instance against the compiled root schema. + * + * Thread-safe (const operation). Must be called after a successful + * set_root_schema(). + * + * @param v Validator + * @param instance JSON instance to validate (borrowed, not modified) + * @param on_error Error callback (may be NULL; errors are still counted) + * @param error_ud User data for error callback + * @param patch_out Output: JSON Patch (RFC 6902) array of default-value + * insertions as json_t* (new reference). May be NULL + * if the caller does not need defaults. + * @return The number of validation errors (0 = valid) + */ +CELIX_JANSSON_EXT_EXPORT int celix_jansson_schema_validate(celix_jansson_schema_validator_t* v, + json_t* instance, + celix_jansson_schema_error_fn on_error, + void* error_ud, + json_t** patch_out); + +/** + * Validate a JSON instance against a specific subschema identified by URI. + * + * The URI typically contains a JSON Pointer fragment, e.g., + * "#/definitions/MyType". + * + * @param v Validator + * @param instance JSON instance to validate + * @param initial_uri URI of the subschema to validate against + * @param on_error Error callback (may be NULL) + * @param error_ud User data for error callback + * @param patch_out Output: JSON Patch array (may be NULL) + * @return Number of validation errors (0 = valid) + */ +CELIX_JANSSON_EXT_EXPORT int celix_jansson_schema_validate_uri(celix_jansson_schema_validator_t* v, + json_t* instance, + const char* initial_uri, + celix_jansson_schema_error_fn on_error, + void* error_ud, + json_t** patch_out); + +/* ──────────────────────────────────────────────────────────────────────── + * Built-in draft-7 meta-schema + * ──────────────────────────────────────────────────────────────────────── */ + +/** + * Returns a new reference to the embedded JSON Schema draft-7 meta-schema. + * + * Useful as the return value for a schema_loader when the requested URI is + * "http://json-schema.org/draft-07/schema". + */ +CELIX_JANSSON_EXT_EXPORT json_t* celix_jansson_schema_draft7_meta_schema(void); + +#ifdef __cplusplus +} +#endif + +#endif /* CELIX_CELIX_JANSSON_SCHEMA_H */ diff --git a/libs/jansson_ext/include/celix_json_merge_patch.h b/libs/jansson_ext/include/celix_json_merge_patch.h new file mode 100644 index 000000000..ba8d96456 --- /dev/null +++ b/libs/jansson_ext/include/celix_json_merge_patch.h @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#ifndef CELIX_CELIX_JSON_MERGE_PATCH_H +#define CELIX_CELIX_JSON_MERGE_PATCH_H + +#include "celix_jansson_ext_export.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Apply a JSON Merge Patch (RFC 7396) to a JSON document. + * + * Produces a new document; neither @p target nor @p patch is modified. + * Semantics follow RFC 7396 section 2 exactly: + * - If @p patch is an object: if @p target is not an object, it is + * treated as an empty object; then for each member of @p patch, a + * null value removes the member from the target (a no-op if the + * member is absent), and any other value recursively merges into + * the target member (an absent member starts from null). + * - If @p patch is not an object (array, string, number, boolean or + * null): the result is a deep copy of @p patch, i.e. the patch + * replaces the whole document. + * + * Cyclic JSON structures are not supported and yield NULL (jansson's + * deep copy rejects them). + * + * @param target The target document (borrowed, never modified) + * @param patch The merge patch (borrowed, never modified) + * @return A new json_t* with the patch applied, or NULL on invalid + * argument or out-of-memory. On out-of-memory the partially + * built result is released. The caller must json_decref() the + * result. + */ +CELIX_JANSSON_EXT_EXPORT json_t* celix_json_merge_patch(const json_t* target, const json_t* patch); + +#ifdef __cplusplus +} +#endif + +#endif /* CELIX_CELIX_JSON_MERGE_PATCH_H */ diff --git a/libs/jansson_ext/include/celix_json_patch.h b/libs/jansson_ext/include/celix_json_patch.h new file mode 100644 index 000000000..2694b9c26 --- /dev/null +++ b/libs/jansson_ext/include/celix_json_patch.h @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#ifndef CELIX_CELIX_JSON_PATCH_H +#define CELIX_CELIX_JSON_PATCH_H + +#include "celix_jansson_ext_export.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Append an "add" operation (RFC 6902) to the patch array. + * + * The patch array is modified in-place. The @p value is consumed — + * ownership is transferred to the patch (steal semantics). On failure + * (invalid @p patch, or out-of-memory while building the operation) the + * @p value is released and ownership is not transferred. + * The stored value will be deep-copied later when the patch is applied + * via celix_json_patch_apply(). + * + * @param patch The JSON Patch array (json_t* array, modified in-place) + * @param path_str JSON Pointer path for the operation (e.g., "/a/b") + * @param value The value to set at the path (ownership is taken on + * success, released on failure) + * @return 0 on success, -1 if @p patch is NULL or not an array, or on + * out-of-memory + */ +CELIX_JANSSON_EXT_EXPORT int celix_json_patch_add(json_t* patch, const char* path_str, json_t* value); + +/** + * Append a "replace" operation (RFC 6902) to the patch array. + * + * Like celix_json_patch_add(), @p value is consumed — ownership is + * transferred to the patch. On failure (invalid @p patch, or + * out-of-memory while building the operation) the @p value is released + * and ownership is not transferred. + * At application time the value is deep-copied into the target document. + * + * @param patch The JSON Patch array (json_t* array, modified in-place) + * @param path_str JSON Pointer path for the operation (e.g., "/a/b") + * @param value The new value (ownership is taken on success, released + * on failure) + * @return 0 on success, -1 if @p patch is NULL or not an array, or on + * out-of-memory + */ +CELIX_JANSSON_EXT_EXPORT int celix_json_patch_replace(json_t* patch, const char* path_str, json_t* value); + +/** + * Append a "remove" operation (RFC 6902) to the patch array. + * + * @param patch The JSON Patch array (json_t* array, modified in-place) + * @param path_str JSON Pointer path of the value to remove + * @return 0 on success, -1 if @p patch is NULL or not an array, or on + * out-of-memory + */ +CELIX_JANSSON_EXT_EXPORT int celix_json_patch_remove(json_t* patch, const char* path_str); + +/** + * Truncate the patch array back to @p old_size entries. + * + * Used internally for combinator rollback (e.g., anyOf/oneOf) to discard + * patch entries from failed branches. If the array has fewer than + * @p old_size entries, this is a no-op. + * + * @param patch The JSON Patch array (json_t* array, modified in-place) + * @param old_size Target number of entries to retain + */ +CELIX_JANSSON_EXT_EXPORT void celix_json_patch_truncate(json_t* patch, size_t old_size); + +/** + * Apply a JSON Patch (RFC 6902) to a JSON document. + * + * The patch is an array of operations as returned by celix_jansson_schema_validate(). + * The original document is NOT modified — a patched copy is returned. + * + * Supported operations: "add", "remove", "replace". + * + * @param original The original JSON document (not modified) + * @param patch JSON Patch array (e.g., from celix_jansson_schema_validate) + * @return A new json_t* with the patch applied, or NULL on error. + * On out-of-memory the partially built result is released and + * NULL is returned. The caller must json_decref() the result. + */ +CELIX_JANSSON_EXT_EXPORT json_t* celix_json_patch_apply(json_t* original, json_t* patch); + +#ifdef __cplusplus +} +#endif + +#endif /* CELIX_CELIX_JSON_PATCH_H */ diff --git a/libs/jansson_ext/src/celix_jansson_pointer.c b/libs/jansson_ext/src/celix_jansson_pointer.c new file mode 100644 index 000000000..28580137e --- /dev/null +++ b/libs/jansson_ext/src/celix_jansson_pointer.c @@ -0,0 +1,607 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "celix_jansson_pointer.h" +#include +#include +#include + +static int is_digit(char c) { return c >= '0' && c <= '9'; } + +static int ensure_cap(celix_json_pointer_t* p, size_t needed) { + if (p->cap >= needed) + return 0; + size_t nc = p->cap ? p->cap * 2 : 8; + while (nc < needed) + nc *= 2; + char** nt = (char**)realloc(p->tokens, nc * sizeof(char*)); + if (!nt) + return -1; + p->tokens = nt; + p->cap = nc; + return 0; +} + +/* ── Lifecycle ─────────────────────────────────────────────────────── */ + +celix_json_pointer_t* celix_json_pointer_create(const char* ptr_str) { + celix_json_pointer_t* p = (celix_json_pointer_t*)calloc(1, sizeof(*p)); + if (!p) + return NULL; + if (ptr_str && celix_json_pointer_init(p, ptr_str) != 0) { + celix_json_pointer_destroy(p); + return NULL; + } + return p; +} + +int celix_json_pointer_init(celix_json_pointer_t* p, const char* ptr_str) { + if (!p) + return -1; + memset(p, 0, sizeof(*p)); /* safe on uninitialized stack memory; callers + reusing a live pointer must clear() it first */ + if (!ptr_str || *ptr_str == '\0') + return 0; + if (*ptr_str != '/') + return -1; + + const char* s = ptr_str + 1; + if (*s == '\0') { + if (celix_json_pointer_push(p, "") != 0) { + celix_json_pointer_clear(p); + return -1; + } + return 0; + } + + int ret = -1; + while (*s) { + const char* tok_start = s; + while (*s && *s != '/') + s++; + size_t tok_len = (size_t)(s - tok_start); + + char* decoded = (char*)malloc(tok_len + 1); + if (!decoded) + goto error; + + size_t di = 0; + for (size_t si = 0; si < tok_len; si++) { + if (tok_start[si] == '~') { + if (si + 1 >= tok_len) { + free(decoded); + goto error; + } + if (tok_start[si + 1] == '0') { + decoded[di++] = '~'; + si++; + } else if (tok_start[si + 1] == '1') { + decoded[di++] = '/'; + si++; + } else { + free(decoded); + goto error; + } + } else { + decoded[di++] = tok_start[si]; + } + } + decoded[di] = '\0'; + + /* RFC 6901: array-index tokens must not have leading zeros */ + if (decoded[0] == '0' && decoded[1] != '\0') { + bool all_digits = true; + for (size_t j = 0; decoded[j]; j++) + if (!is_digit(decoded[j])) { + all_digits = false; + break; + } + if (all_digits) { + free(decoded); + goto error; + } + } + + if (celix_json_pointer_push(p, decoded) != 0) { + free(decoded); + goto error; + } + free(decoded); + + if (*s == '/') { + s++; + if (*s == '\0') { /* trailing slash → empty final token */ + if (celix_json_pointer_push(p, "") != 0) + goto error; + } + } + } + ret = 0; + +error: + if (ret != 0) + celix_json_pointer_clear(p); + return ret; +} + +celix_json_pointer_t* celix_json_pointer_copy(const celix_json_pointer_t* src) { + if (!src) + return NULL; + celix_json_pointer_t* dst = (celix_json_pointer_t*)calloc(1, sizeof(*dst)); + if (!dst) + return NULL; + if (ensure_cap(dst, src->len) != 0) { + free(dst); + return NULL; + } + for (size_t i = 0; i < src->len; i++) { + dst->tokens[i] = strdup(src->tokens[i]); + if (!dst->tokens[i]) { + celix_json_pointer_destroy(dst); + return NULL; + } + dst->len++; + } + return dst; +} + +void celix_json_pointer_destroy(celix_json_pointer_t* ptr) { + if (!ptr) + return; + celix_json_pointer_clear(ptr); + free(ptr); +} + +void celix_json_pointer_clear(celix_json_pointer_t* ptr) { + if (!ptr) + return; + for (size_t i = 0; i < ptr->len; i++) + free(ptr->tokens[i]); + free(ptr->tokens); + memset(ptr, 0, sizeof(*ptr)); +} + +/* ── Inspection ─────────────────────────────────────────────────────── */ + +size_t celix_json_pointer_depth(const celix_json_pointer_t* ptr) { return ptr ? ptr->len : 0; } + +const char* celix_json_pointer_token(const celix_json_pointer_t* ptr, size_t idx) { + return (ptr && idx < ptr->len) ? ptr->tokens[idx] : NULL; +} + +/* ── Mutation ───────────────────────────────────────────────────────── */ + +int celix_json_pointer_push(celix_json_pointer_t* ptr, const char* token) { + if (!ptr || !token || ensure_cap(ptr, ptr->len + 1) != 0) + return -1; + ptr->tokens[ptr->len] = strdup(token); + if (!ptr->tokens[ptr->len]) + return -1; + ptr->len++; + return 0; +} + +void celix_json_pointer_pop(celix_json_pointer_t* ptr) { + if (!ptr || ptr->len == 0) + return; + ptr->len--; + free(ptr->tokens[ptr->len]); + ptr->tokens[ptr->len] = NULL; +} + +/* ── Serialization ──────────────────────────────────────────────────── */ + +char* celix_json_pointer_to_string(const celix_json_pointer_t* ptr) { + if (!ptr) + return NULL; + + size_t total = 1; + for (size_t i = 0; i < ptr->len; i++) { + for (const char* c = ptr->tokens[i]; *c; c++) + total += (*c == '~' || *c == '/') ? 2 : 1; + if (i < ptr->len - 1) + total++; + } + + char* out = (char*)malloc(total + 1); + if (!out) + return NULL; + char* w = out; + if (ptr->len == 0) { + *w++ = '/'; + *w = '\0'; + } else { + for (size_t i = 0; i < ptr->len; i++) { + *w++ = '/'; + for (const char* c = ptr->tokens[i]; *c; c++) { + if (*c == '~') { + *w++ = '~'; + *w++ = '0'; + } else if (*c == '/') { + *w++ = '~'; + *w++ = '1'; + } else + *w++ = *c; + } + } + } + *w = '\0'; + return out; +} + +/* ── Document access ────────────────────────────────────────────────── */ + +json_t* celix_json_pointer_get(json_t* doc, const celix_json_pointer_t* ptr) { + if (!doc || !ptr) + return NULL; + json_t* cur = doc; + for (size_t i = 0; i < ptr->len; i++) { + if (json_is_object(cur)) { + cur = json_object_get(cur, ptr->tokens[i]); + } else if (json_is_array(cur)) { + const char* tok = ptr->tokens[i]; + if (*tok == '-') + return NULL; + for (const char* c = tok; *c; c++) + if (!is_digit(*c)) + return NULL; + size_t idx = (size_t)strtoul(tok, NULL, 10); + cur = json_array_get(cur, idx); + } else + return NULL; + if (!cur) + return NULL; + } + return cur; +} + +int celix_json_pointer_contains(json_t* doc, const celix_json_pointer_t* ptr) { + return celix_json_pointer_get(doc, ptr) != NULL; +} + +/* ── get_or_create ──────────────────────────────────────────────────── */ + +json_t* celix_json_pointer_get_or_create(json_t* doc, const celix_json_pointer_t* ptr) { + if (!doc || !ptr || !(json_is_object(doc) || json_is_array(doc))) + return NULL; + + json_t* cur = doc; + for (size_t i = 0; i < ptr->len; i++) { + const char* tok = ptr->tokens[i]; + bool is_last = (i == ptr->len - 1); + + if (json_is_object(cur)) { + json_t* child = json_object_get(cur, tok); + if (!child) { + if (is_last) { + child = json_null(); + if (!child) + return NULL; + if (json_object_set(cur, tok, child) != 0) { + json_decref(child); + return NULL; + } + return child; + } + bool is_num = true; + if (*tok == '\0') + is_num = false; + for (const char* c = tok; *c; c++) + if (!is_digit(*c)) { + is_num = false; + break; + } + if (is_num && tok[0] == '0' && tok[1] != '\0') + is_num = false; + child = is_num ? json_array() : json_object(); + if (!child || json_object_set_new(cur, tok, child) != 0) + return NULL; + } + cur = child; + } else if (json_is_array(cur)) { + if (strcmp(tok, "-") == 0) { + if (is_last) { + json_t* n = json_null(); + if (!n || json_array_append_new(cur, n) != 0) + return NULL; + json_incref(cur); + return cur; + } + return NULL; + } + for (const char* c = tok; *c; c++) + if (!is_digit(*c)) + return NULL; + size_t idx = (size_t)strtoul(tok, NULL, 10); + while (json_array_size(cur) <= idx) { + json_t* n = json_null(); + if (!n || json_array_append_new(cur, n) != 0) + return NULL; + } + cur = json_array_get(cur, idx); + } else + return NULL; + } + json_incref(cur); + return cur; +} + +/* ── set ────────────────────────────────────────────────────────────── */ + +int celix_json_pointer_set(json_t* doc, const celix_json_pointer_t* ptr, json_t* value) { + if (!doc || !ptr || ptr->len == 0) + return -1; + + json_t* cur = doc; + for (size_t i = 0; i < ptr->len - 1; i++) { + const char* tok = ptr->tokens[i]; + + if (json_is_object(cur)) { + json_t* child = json_object_get(cur, tok); + if (!child) { + /* Look ahead: if next token is numeric → array, else object */ + bool next_num = false; + if (i + 1 < ptr->len) { + const char* next = ptr->tokens[i + 1]; + next_num = true; + if (*next == '\0') + next_num = false; + for (const char* c = next; *c; c++) + if (!is_digit(*c)) { + next_num = false; + break; + } + if (next_num && next[0] == '0' && next[1] != '\0') + next_num = false; + } + child = next_num ? json_array() : json_object(); + if (!child || json_object_set_new(cur, tok, child) != 0) { + json_decref(value); + return -1; + } + } + cur = child; + } else if (json_is_array(cur)) { + for (const char* c = tok; *c; c++) + if (!is_digit(*c)) { + json_decref(value); + return -1; + } + size_t idx = (size_t)strtoul(tok, NULL, 10); + while (json_array_size(cur) <= idx) { + json_t* n = json_null(); + if (!n || json_array_append_new(cur, n) != 0) { + json_decref(value); + return -1; + } + } + json_t* child = json_array_get(cur, idx); + /* Replace null/primitive intermediates with containers */ + if (!child || (!json_is_object(child) && !json_is_array(child))) { + bool next_num = false; + if (i + 1 < ptr->len) { + const char* next = ptr->tokens[i + 1]; + next_num = true; + if (*next == '\0') + next_num = false; + for (const char* c = next; *c; c++) + if (!is_digit(*c)) { + next_num = false; + break; + } + if (next_num && next[0] == '0' && next[1] != '\0') + next_num = false; + } + json_t* repl = next_num ? json_array() : json_object(); + if (!repl || json_array_set_new(cur, idx, repl) != 0) { + json_decref(value); + return -1; + } + child = repl; + } + cur = child; + } else { + json_decref(value); + return -1; + } + } + + const char* last = ptr->tokens[ptr->len - 1]; + if (json_is_object(cur)) { + if (json_object_set_new(cur, last, value) != 0) + return -1; + return 0; + } + if (json_is_array(cur)) { + if (strcmp(last, "-") == 0) { + if (json_array_append_new(cur, value) != 0) + return -1; + return 0; + } + for (const char* c = last; *c; c++) + if (!is_digit(*c)) { + json_decref(value); + return -1; + } + size_t idx = (size_t)strtoul(last, NULL, 10); + while (json_array_size(cur) <= idx) { + json_t* n = json_null(); + if (!n || json_array_append_new(cur, n) != 0) { + json_decref(value); + return -1; + } + } + if (json_array_set_new(cur, idx, value) != 0) + return -1; + return 0; + } + json_decref(value); + return -1; +} + +int celix_json_pointer_set_new(json_t* doc, const celix_json_pointer_t* ptr, json_t* value) { + if (value) + json_incref(value); + return celix_json_pointer_set(doc, ptr, value); +} + +/* ── remove ─────────────────────────────────────────────────────────── */ + +int celix_json_pointer_remove(json_t* doc, const celix_json_pointer_t* ptr) { + if (!doc || !ptr || ptr->len == 0) + return -1; + + celix_json_pointer_t parent_ptr; + memset(&parent_ptr, 0, sizeof(parent_ptr)); + for (size_t i = 0; i < ptr->len - 1; i++) { + if (celix_json_pointer_push(&parent_ptr, ptr->tokens[i]) != 0) { + celix_json_pointer_clear(&parent_ptr); + return -1; + } + } + + json_t* parent = celix_json_pointer_get(doc, &parent_ptr); + celix_json_pointer_clear(&parent_ptr); + if (!parent) + return -1; + + const char* last = ptr->tokens[ptr->len - 1]; + if (json_is_object(parent)) { + if (!json_object_get(parent, last)) + return -1; + json_object_del(parent, last); + return 0; + } + if (json_is_array(parent)) { + for (const char* c = last; *c; c++) + if (!is_digit(*c)) + return -1; + size_t idx = (size_t)strtoul(last, NULL, 10); + if (idx >= json_array_size(parent)) + return -1; + json_array_remove(parent, idx); + return 0; + } + return -1; +} + +/* ── Escape / unescape ──────────────────────────────────────────────── */ + +char* celix_json_pointer_escape(const char* token) { + if (!token) + return NULL; + size_t len = 0; + for (const char* c = token; *c; c++) + len += (*c == '~' || *c == '/') ? 2 : 1; + char* out = (char*)malloc(len + 1); + if (!out) + return NULL; + char* w = out; + for (const char* c = token; *c; c++) { + if (*c == '~') { + *w++ = '~'; + *w++ = '0'; + } else if (*c == '/') { + *w++ = '~'; + *w++ = '1'; + } else + *w++ = *c; + } + *w = '\0'; + return out; +} + +char* celix_json_pointer_unescape(const char* token) { + if (!token) + return NULL; + size_t len = strlen(token); + char* out = (char*)malloc(len + 1); + if (!out) + return NULL; + size_t di = 0; + for (size_t si = 0; si < len; si++) { + if (token[si] == '~' && si + 1 < len) { + if (token[si + 1] == '0') { + out[di++] = '~'; + si++; + } else if (token[si + 1] == '1') { + out[di++] = '/'; + si++; + } else { + out[di++] = '~'; + } + } else + out[di++] = token[si]; + } + out[di] = '\0'; + return out; +} + +/* ── Navigation ─────────────────────────────────────────────────────── */ + +celix_json_pointer_t* celix_json_pointer_parent(const celix_json_pointer_t* ptr, celix_json_pointer_t* out) { + if (!ptr || ptr->len == 0) + return NULL; + celix_json_pointer_t* r = out ? out : (celix_json_pointer_t*)calloc(1, sizeof(*r)); + if (!r) + return NULL; + if (out) + celix_json_pointer_clear(out); /* caller-provided buffer is cleared first */ + if (ensure_cap(r, ptr->len - 1) != 0) { + if (!out) + free(r); + return NULL; + } + for (size_t i = 0; i < ptr->len - 1; i++) { + r->tokens[i] = strdup(ptr->tokens[i]); + if (!r->tokens[i]) { + celix_json_pointer_clear(r); + if (!out) + free(r); + return NULL; + } + r->len++; + } + return r; +} + +int celix_json_pointer_concat(celix_json_pointer_t* ptr, const celix_json_pointer_t* suffix) { + if (!ptr || !suffix) + return -1; + /* on OOM, suffix tokens appended so far remain */ + for (size_t i = 0; i < suffix->len; i++) + if (celix_json_pointer_push(ptr, suffix->tokens[i]) != 0) + return -1; + return 0; +} + +/* ── Comparison ─────────────────────────────────────────────────────── */ + +int celix_json_pointer_equals(const celix_json_pointer_t* a, const celix_json_pointer_t* b) { + if (a == b) + return true; + if (!a || !b) + return false; + if (a->len != b->len) + return false; + for (size_t i = 0; i < a->len; i++) { + if (strcmp(a->tokens[i], b->tokens[i]) != 0) + return false; + } + return true; +} diff --git a/libs/jansson_ext/src/celix_jansson_schema.c b/libs/jansson_ext/src/celix_jansson_schema.c new file mode 100644 index 000000000..51e3debbe --- /dev/null +++ b/libs/jansson_ext/src/celix_jansson_schema.c @@ -0,0 +1,2986 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "celix_jansson_schema.h" +#include "celix_json_patch.h" +#include "celix_jansson_uri.h" +#include "celix_schema.h" +#include "celix_stdlib_cleanup.h" +#include "celix_string_format_check.h" +#include "celix_util.h" +#include +#include +#include +#include +#include +#include +#include + +/* ════════════════════════════════════════════════════════════════════════ + * Error codes + * ════════════════════════════════════════════════════════════════════════ */ + +const char* celix_jansson_schema_strerror(int err) { + switch (err) { + case CELIX_JANSSON_SCHEMA_OK: + return "success"; + case CELIX_JANSSON_SCHEMA_ERROR_NOMEM: + return "allocation failure"; + case CELIX_JANSSON_SCHEMA_ERROR_INVALID_SCHEMA: + return "schema must be boolean or object"; + case CELIX_JANSSON_SCHEMA_ERROR_SCHEMA_PARSE: + return "JSON parse error"; + case CELIX_JANSSON_SCHEMA_ERROR_URI: + return "malformed URI"; + case CELIX_JANSSON_SCHEMA_ERROR_REF_UNRESOLVED: + return "unresolved $ref"; + case CELIX_JANSSON_SCHEMA_ERROR_LOADER: + return "schema loader failed or absent"; + case CELIX_JANSSON_SCHEMA_ERROR_FORMAT_CHECKER: + return "format checker required but not provided"; + case CELIX_JANSSON_SCHEMA_ERROR_CONTENT_CHECKER: + return "content checker required but not provided"; + case CELIX_JANSSON_SCHEMA_ERROR_DUPLICATE_URI: + return "duplicate URI"; + case CELIX_JANSSON_SCHEMA_ERROR_INVALID_PATTERN: + return "invalid regex pattern"; + case CELIX_JANSSON_SCHEMA_ERROR_NO_ROOT_SCHEMA: + return "no root schema set"; + case CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT: + return "invalid argument"; + default: + return "unknown error"; + } +} + +/* ════════════════════════════════════════════════════════════════════════ + * Object node helpers + * ════════════════════════════════════════════════════════════════════════ */ + +/* The properties/dependencies maps are created lazily (only when the matching + * schema keyword is present), so they can be NULL for a valid object node. */ +static celix_jansson_schema_node_t* obj_node_get(celix_string_hash_map_t* map, const char* key) { + return map ? (celix_jansson_schema_node_t*)celix_stringHashMap_get(map, key) : NULL; +} + +/* Create a string hash map whose values are owning schema node refs; the + * removed callback unrefs values when they leave the map. Returns NULL on OOM. */ +static celix_string_hash_map_t* obj_node_map_create(void) { + celix_string_hash_map_create_options_t opts = CELIX_EMPTY_STRING_HASH_MAP_CREATE_OPTIONS; + opts.simpleRemovedCallback = (void (*)(void*))celix_jansson_schema_unref; + return celix_stringHashMap_createWithOptions(&opts); +} + +/* ════════════════════════════════════════════════════════════════════════ + * Path stack + * ════════════════════════════════════════════════════════════════════════ */ + +void celix_jansson_path_init(celix_jansson_path_t* p) { memset(p, 0, sizeof(*p)); } + +int celix_jansson_path_push(celix_jansson_path_t* p, const char* token) { + if (p->len >= p->cap) { + size_t nc = p->cap ? p->cap * 2 : 8; + char** nt = (char**)realloc(p->tokens, nc * sizeof(char*)); + if (!nt) + return -1; + p->tokens = nt; + p->cap = nc; + } + p->tokens[p->len] = strdup(token); + if (!p->tokens[p->len]) + return -1; + p->len++; + free(p->cached); + p->cached = NULL; + return 0; +} + +void celix_jansson_path_pop(celix_jansson_path_t* p) { + if (p->len > 0) { + p->len--; + free(p->tokens[p->len]); + p->tokens[p->len] = NULL; + } + free(p->cached); + p->cached = NULL; +} + +const char* celix_jansson_path_str(celix_jansson_path_t* p) { + if (!p->cached) { + /* OOM: any append failure aborts the build and surfaces as a NULL + * return (the callers treat NULL as OOM and degrade gracefully). + * p->cached stays NULL, so a later call can retry; the strbuf is + * released automatically on every exit path. */ + celix_auto(celix_jansson_strbuf_t) sb; + celix_jansson_strbuf_init(&sb); + for (size_t i = 0; i < p->len; i++) { + int rc; + rc = celix_jansson_strbuf_appendc(&sb, '/'); + if (rc != 0) + return NULL; + for (const char* c = p->tokens[i]; *c; c++) { + if (*c == '~') + rc = celix_jansson_strbuf_appends(&sb, "~0"); + else if (*c == '/') + rc = celix_jansson_strbuf_appends(&sb, "~1"); + else + rc = celix_jansson_strbuf_appendc(&sb, *c); + if (rc != 0) + return NULL; + } + } + p->cached = celix_jansson_strbuf_detach(&sb); + if (!p->cached) + p->cached = strdup(""); /* keep: an empty path is "" rather than NULL */ + } + return p->cached; +} + +void celix_jansson_path_free(celix_jansson_path_t* p) { + for (size_t i = 0; i < p->len; i++) + free(p->tokens[i]); + free(p->tokens); + free(p->cached); + memset(p, 0, sizeof(*p)); +} + +/* ════════════════════════════════════════════════════════════════════════ + * Reference counting + * ════════════════════════════════════════════════════════════════════════ */ + +celix_jansson_schema_node_t* celix_jansson_schema_ref(celix_jansson_schema_node_t* n) { + if (n) + __atomic_fetch_add(&n->refcount, 1, __ATOMIC_SEQ_CST); + return n; +} + +void celix_jansson_schema_unref(celix_jansson_schema_node_t* n) { + if (!n) + return; + if (__atomic_fetch_sub(&n->refcount, 1, __ATOMIC_SEQ_CST) == 1) { + if (n->vtable && n->vtable->destroy) + n->vtable->destroy(n); + } +} + +/* ════════════════════════════════════════════════════════════════════════ + * Error sinks (user, first-error, collecting) + * ════════════════════════════════════════════════════════════════════════ */ + +static void +emit_error_v(celix_jansson_validation_context_t* ctx, celix_jansson_path_t* path, const char* fmt, va_list ap) { + char* msg = NULL; + const char* text; + if (vasprintf(&msg, fmt, ap) >= 0) + text = msg; + else { + /* OOM: the formatted message is unavailable; surface a constant hint + * (allocating the fallback would likely fail for the same reason) */ + text = "out of memory: error message unavailable"; + msg = NULL; /* glibc leaves *ptr undefined on failure */ + } + const char* ps = path ? celix_jansson_path_str(path) : ""; + if (!ps) + ps = ""; /* path_str can return NULL only if its empty-string fallback allocation failed */ + ctx->sink->emit(ctx->sink, ps, NULL, text); + free(msg); +} + +static void emit_error(celix_jansson_validation_context_t* ctx, celix_jansson_path_t* path, const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + emit_error_v(ctx, path, fmt, ap); + va_end(ap); +} + +/* -- user sink -- */ +typedef struct { + celix_jansson_error_sink_t base; + celix_jansson_schema_error_fn fn; + void* ud; + int count; + bool abort_on_error; + celix_jansson_validation_context_t* ctx; /* back-pointer for setting ctx->aborted */ +} user_sink_t; +static void user_emit(celix_jansson_error_sink_t* s, const char* p, json_t* i, const char* m) { + user_sink_t* us = (user_sink_t*)s; + if (us->fn) + us->fn(p, i, m, us->ud); + us->count++; + if (us->abort_on_error && us->ctx) { + us->ctx->aborted = true; + } +} +static void user_destroy(celix_jansson_error_sink_t* s) { free(s); } + +/* -- first-error sink -- */ +typedef struct { + celix_jansson_error_sink_t base; + bool got; + char* ptr; + json_t* inst; + char* msg; +} first_sink_t; +static void first_emit(celix_jansson_error_sink_t* s, const char* p, json_t* i, const char* m) { + first_sink_t* fs = (first_sink_t*)s; + if (!fs->got) { + fs->got = true; + fs->ptr = strdup(p); + fs->msg = strdup(m); + fs->inst = i; + json_incref(i); + } +} +static void first_destroy(celix_jansson_error_sink_t* s) { + first_sink_t* fs = (first_sink_t*)s; + free(fs->ptr); + json_decref(fs->inst); + free(fs->msg); + free(fs); +} + +/* Allocate a first-error sink. Returns NULL on OOM. */ +static first_sink_t* first_sink_new(void) { + first_sink_t* fs = (first_sink_t*)calloc(1, sizeof(*fs)); + if (!fs) + return NULL; + fs->base.emit = first_emit; + fs->base.destroy = first_destroy; + return fs; +} + +/* -- collecting sink -- */ +typedef struct { + celix_jansson_error_sink_t base; + celix_jansson_error_list_t list; +} collecting_sink_t; +static void coll_emit(celix_jansson_error_sink_t* s, const char* p, json_t* i, const char* m) { + collecting_sink_t* cs = (collecting_sink_t*)s; + celix_jansson_error_list_add(&cs->list, p, i, m); +} +static void coll_destroy(celix_jansson_error_sink_t* s) { + collecting_sink_t* cs = (collecting_sink_t*)s; + celix_jansson_error_list_clear(&cs->list); + free(cs); +} +static void coll_propagate(collecting_sink_t* cs, celix_jansson_error_sink_t* parent, const char* prefix) { + for (size_t i = 0; i < cs->list.len; i++) { + celix_jansson_error_entry_t* e = &cs->list.entries[i]; + celix_auto(celix_jansson_strbuf_t) sb; + celix_jansson_strbuf_init(&sb); + /* strdup inside error_list_add can fail, leaving NULL message/ptr: + * fall back to an empty string so appends(NULL)/emit(NULL) never happen */ + /* OOM: an append failure leaves full == NULL and the raw message is + * emitted instead (only the prefix is lost, never a crash or a + * truncated message); sb is released automatically. */ + char* full = NULL; + if ((!prefix || celix_jansson_strbuf_appends(&sb, prefix) == 0) && + celix_jansson_strbuf_appends(&sb, e->message ? e->message : "") == 0) { + full = celix_jansson_strbuf_detach(&sb); + } + parent->emit(parent, e->ptr ? e->ptr : "", e->instance, full ? full : (e->message ? e->message : "")); + free(full); + } +} + +static collecting_sink_t* coll_new(void) { + collecting_sink_t* cs = (collecting_sink_t*)calloc(1, sizeof(*cs)); + if (!cs) + return NULL; + cs->base.emit = coll_emit; + cs->base.destroy = coll_destroy; + celix_jansson_error_list_init(&cs->list); + return cs; +} + +/* ════════════════════════════════════════════════════════════════════════ + * Helpers + * ════════════════════════════════════════════════════════════════════════ */ + +/* Cross-type numeric equality (integer vs float only) */ +static bool jss_json_equal(json_t* a, json_t* b) { + if (json_equal(a, b)) + return true; + /* Integer vs real: compare numeric values (e.g., 0 == 0.0) */ + if ((json_is_integer(a) && json_is_real(b)) || (json_is_real(a) && json_is_integer(b))) { + double va = json_is_real(a) ? json_real_value(a) : (double)json_integer_value(a); + double vb = json_is_real(b) ? json_real_value(b) : (double)json_integer_value(b); + return va == vb; + } + return false; +} + +int celix_jansson_type_index(json_t* value) { + switch (json_typeof(value)) { + case JSON_NULL: + return 0; + case JSON_OBJECT: + return 1; + case JSON_ARRAY: + return 2; + case JSON_STRING: + return 3; + case JSON_TRUE: + case JSON_FALSE: + return 4; + case JSON_INTEGER: + return 5; + case JSON_REAL: + return 6; + //LCOV_EXCL_START: all legal jansson types are covered by the cases above + default: + return -1; + //LCOV_EXCL_STOP + } +} + +/* Build a child path by copying the parent's tokens and appending one more. + * Fail-closes on OOM: reports the failure through the sink and returns false + * so the caller aborts the current validation pass instead of descending with + * a truncated path. */ +static bool path_child_checked(celix_jansson_path_t* child, + celix_jansson_path_t* parent, + const char* token, + celix_jansson_validation_context_t* ctx, + int* errs) { + for (size_t i = 0; i < parent->len; i++) { + if (celix_jansson_path_push(child, parent->tokens[i]) != 0) { + emit_error(ctx, parent, "out of memory while building instance path"); + (*errs)++; + return false; + } + } + if (celix_jansson_path_push(child, token) != 0) { + emit_error(ctx, parent, "out of memory while building instance path"); + (*errs)++; + return false; + } + return true; +} + +/* UTF-8 code point count (count non-continuation bytes) */ +static size_t utf8_length(const char* s, size_t len) { + size_t n = 0; + for (size_t i = 0; i < len; i++) + if ((s[i] & 0xC0) != 0x80) + n++; + return n; +} + +/* Floating-point multipleOf check with epsilon */ +static bool violates_multiple(double x, double m) { + if (m == 0.0) + return false; + double r = remainder(x, m); + double mult = fabs(x / m); + if (mult > 1.0) + r /= mult; + double eps = fabs(nextafter(x, 0.0) - x); + return fabs(r) > fabs(eps); +} + +/* ════════════════════════════════════════════════════════════════════════ + * Validation destructors + * ════════════════════════════════════════════════════════════════════════ */ + +static void d_boolean(celix_jansson_schema_node_t* n) { free(n); } +static void d_null(celix_jansson_schema_node_t* n) { free(n); } +static void d_booltype(celix_jansson_schema_node_t* n) { free(n); } + +static void d_required(celix_jansson_schema_node_t* n) { + for (size_t i = 0; i < n->u.required.len; i++) + free(n->u.required.names[i]); + free(n->u.required.names); + free(n); +} + +static void d_ref(celix_jansson_schema_node_t* n) { + free(n->u.ref.id); + json_decref(n->default_value); + free(n); +} + +static void d_type(celix_jansson_schema_node_t* n) { + for (int i = 0; i < 7; i++) + celix_jansson_schema_unref(n->u.type_schema.type_slots[i]); + for (size_t i = 0; i < n->u.type_schema.logic_len; i++) + celix_jansson_schema_unref(n->u.type_schema.logic[i]); + free(n->u.type_schema.logic); + celix_jansson_schema_unref(n->u.type_schema.if_schema); + celix_jansson_schema_unref(n->u.type_schema.then_schema); + celix_jansson_schema_unref(n->u.type_schema.else_schema); + json_decref(n->u.type_schema.enum_values); + json_decref(n->u.type_schema.const_value); + json_decref(n->default_value); + free(n); +} + +static void d_str(celix_jansson_schema_node_t* n) { + if (n->u.string.has_pattern) + regfree(&n->u.string.pattern); + free(n->u.string.pattern_str); + free(n->u.string.format); + free(n->u.string.content_encoding); + free(n->u.string.content_media_type); + json_decref(n->default_value); + free(n); +} + +static void d_numeric(celix_jansson_schema_node_t* n) { + json_decref(n->default_value); + free(n); +} + +static void d_object(celix_jansson_schema_node_t* n) { + for (size_t i = 0; i < n->u.object.required_len; i++) + free(n->u.object.required[i]); + free(n->u.object.required); + celix_stringHashMap_destroy(n->u.object.properties); /* NULL-safe; values freed via creation-time removed callback */ + for (size_t i = 0; i < n->u.object.pp_len; i++) { + regfree(&n->u.object.pattern_properties[i].re); + celix_jansson_schema_unref(n->u.object.pattern_properties[i].sch); + } + free(n->u.object.pattern_properties); + celix_jansson_schema_unref(n->u.object.additional_properties); + celix_stringHashMap_destroy(n->u.object.dependencies); + celix_jansson_schema_unref(n->u.object.property_names); + json_decref(n->default_value); + free(n); +} + +static void d_array(celix_jansson_schema_node_t* n) { + celix_jansson_schema_unref(n->u.array.items_schema); + for (size_t i = 0; i < n->u.array.items_len; i++) + celix_jansson_schema_unref(n->u.array.items[i]); + free(n->u.array.items); + celix_jansson_schema_unref(n->u.array.additional_items); + celix_jansson_schema_unref(n->u.array.contains); + json_decref(n->default_value); + free(n); +} + +static void d_not(celix_jansson_schema_node_t* n) { + celix_jansson_schema_unref(n->u.not_schema.sub); + free(n); +} + +static void d_comb(celix_jansson_schema_node_t* n) { + for (size_t i = 0; i < n->u.combination.len; i++) + celix_jansson_schema_unref(n->u.combination.items[i]); + free(n->u.combination.items); + free(n); +} + +/* ════════════════════════════════════════════════════════════════════════ + * Per-kind validators + * ════════════════════════════════════════════════════════════════════════ */ + +/* -- boolean -- */ +static int v_boolean(const celix_jansson_schema_node_t* n, + json_t* inst, + celix_jansson_path_t* p, + celix_jansson_validation_context_t* ctx) { + if (!n->u.boolean.value) + emit_error(ctx, p, "instance invalid as per false-schema"); + return n->u.boolean.value ? 0 : 1; +} +static const json_t* dv_boolean(const celix_jansson_schema_node_t* n, + celix_jansson_path_t* p, + const json_t* inst, + celix_jansson_validation_context_t* ctx) { + (void)p; + (void)inst; + (void)ctx; + return n->default_value; +} + +static const schema_vtable vt_boolean = {v_boolean, dv_boolean, d_boolean}; + +/* -- null -- */ +static int v_null(const celix_jansson_schema_node_t* n, + json_t* inst, + celix_jansson_path_t* p, + celix_jansson_validation_context_t* ctx) { + (void)n; + (void)inst; /* unused once the assert compiles out under NDEBUG */ + (void)p; + (void)ctx; + assert(json_is_null(inst)); /* only dispatched from type_slots[0] (JSON_NULL) */ + return 0; +} +static const schema_vtable vt_null = {v_null, NULL, d_null}; + +/* -- boolean_type -- */ +static int v_booltype(const celix_jansson_schema_node_t* n, + json_t* inst, + celix_jansson_path_t* p, + celix_jansson_validation_context_t* ctx) { + (void)n; + (void)inst; /* unused once the assert compiles out under NDEBUG */ + (void)p; + (void)ctx; + assert(json_is_boolean(inst)); /* only dispatched from type_slots[4] (JSON_TRUE/JSON_FALSE) */ + return 0; +} +static const schema_vtable vt_booltype = {v_booltype, NULL, d_booltype}; + +/* -- ref -- */ +static int v_ref(const celix_jansson_schema_node_t* n, + json_t* inst, + celix_jansson_path_t* p, + celix_jansson_validation_context_t* ctx) { + /* target_weak is wired up at compile time (placeholder nodes are resolved in + * phase 2), so a root self-ref ("$ref": "#") never needs a runtime fallback. */ + celix_jansson_schema_node_t* t = n->u.ref.target_weak; + if (!t) { + emit_error(ctx, p, "unresolved or freed schema-reference"); + return 1; + } + /* Guard against infinite recursion through circular $ref */ + if (ctx->ref_depth > 20) { + emit_error(ctx, p, "exceeded maximum $ref recursion depth"); + return 1; + } + ctx->ref_depth++; + int rc = t->vtable->validate(t, inst, p, ctx); + ctx->ref_depth--; + return rc; +} +static const json_t* dv_ref(const celix_jansson_schema_node_t* n, + celix_jansson_path_t* p, + const json_t* inst, + celix_jansson_validation_context_t* ctx) { + if (n->default_value) + return n->default_value; + celix_jansson_schema_node_t* t = n->u.ref.target_weak; + if (t && t->vtable->default_value) + return t->vtable->default_value(t, p, inst, ctx); + return NULL; +} +static const schema_vtable vt_ref = {v_ref, dv_ref, d_ref}; + +/* -- required -- */ +static int v_required(const celix_jansson_schema_node_t* n, + json_t* inst, + celix_jansson_path_t* p, + celix_jansson_validation_context_t* ctx) { + assert(json_is_object(inst)); /* deps are only validated from v_object on an object instance */ + int errs = 0; + for (size_t i = 0; i < n->u.required.len; i++) { + json_t* v = json_object_get(inst, n->u.required.names[i]); + if (!v) { + celix_auto(celix_jansson_path_t) pp = {0}; + if (!path_child_checked(&pp, p, n->u.required.names[i], ctx, &errs)) { + return errs; + } + emit_error(ctx, &pp, "required property '%s' not found in object", n->u.required.names[i]); + errs++; + } + } + return errs; +} +static const schema_vtable vt_required = {v_required, NULL, d_required}; + +/* -- string -- */ +static int v_string(const celix_jansson_schema_node_t* n, + json_t* inst, + celix_jansson_path_t* p, + celix_jansson_validation_context_t* ctx) { + assert(json_is_string(inst)); /* only dispatched from type_slots[3] (JSON_STRING) */ + const char* s = json_string_value(inst); + size_t len = strlen(s); + int errs = 0; + + if (n->u.string.has_min_len) { + size_t cplen = utf8_length(s, len); + if (cplen < n->u.string.min_len) { + emit_error(ctx, p, "instance string is too short (min=%zu, got=%zu)", n->u.string.min_len, cplen); + errs++; + } + } + if (n->u.string.has_max_len) { + size_t cplen = utf8_length(s, len); + if (cplen > n->u.string.max_len) { + emit_error(ctx, p, "instance string is too long (max=%zu, got=%zu)", n->u.string.max_len, cplen); + errs++; + } + } + if (n->u.string.has_content) { + celix_jansson_schema_root_t* root = n->root; + /* Reachable: the compile-time guard only checks contentEncoding, so a + * contentMediaType-only schema compiles without a checker. Keep this + * runtime check (covered by ContentMediaTypeWithoutCheckerAtValidate). */ + if (!root->content) { + emit_error( + ctx, p, "a content checker was not provided but a contentEncoding/contentMediaType keyword is present"); + errs++; + } else { + int rc = root->content(n->u.string.content_encoding, + n->u.string.content_media_type ? n->u.string.content_media_type : "", + inst, + root->content_ud); + if (rc != CELIX_JANSSON_SCHEMA_OK) { + emit_error(ctx, p, "content validation failed"); + errs++; + } + } + } + if (n->u.string.has_pattern) { + if (regexec(&n->u.string.pattern, s, 0, NULL, 0) != 0) { + emit_error(ctx, p, "instance string does not match pattern '%s'", n->u.string.pattern_str); + errs++; + } + } + if (n->u.string.has_format) { + celix_jansson_schema_root_t* root = n->root; + assert(root->format != NULL); /* compile-time guard: no checker → CELIX_JANSSON_SCHEMA_ERROR_FORMAT_CHECKER */ + int rc = root->format(n->u.string.format, s, root->format_ud); + if (rc != CELIX_JANSSON_SCHEMA_OK) { + emit_error(ctx, p, "format-checking failed: %s", n->u.string.format); + errs++; + } + } + return errs; +} +static const schema_vtable vt_string = {v_string, NULL, d_str}; + +/* -- numeric (int/float share the same logic, just different bounds types) -- */ +static int v_numeric_int(const celix_jansson_schema_node_t* n, + json_t* inst, + celix_jansson_path_t* p, + celix_jansson_validation_context_t* ctx) { + assert(json_is_integer(inst)); /* only dispatched from type_slots[5] (JSON_INTEGER) */ + json_int_t v = json_integer_value(inst); + int errs = 0; + if (n->u.numeric.has_min) { + if (n->u.numeric.exclusive_min ? (v <= n->u.numeric.bounds.i.min) : (v < n->u.numeric.bounds.i.min)) { + emit_error(ctx, p, "instance is below minimum of %lld", (long long)n->u.numeric.bounds.i.min); + errs++; + } + } + if (n->u.numeric.has_max) { + if (n->u.numeric.exclusive_max ? (v >= n->u.numeric.bounds.i.max) : (v > n->u.numeric.bounds.i.max)) { + emit_error(ctx, p, "instance exceeds maximum of %lld", (long long)n->u.numeric.bounds.i.max); + errs++; + } + } + if (n->u.numeric.has_mult) { + if (violates_multiple((double)v, n->u.numeric.multiple_of)) { + emit_error(ctx, p, "instance is not a multiple of %g", n->u.numeric.multiple_of); + errs++; + } + } + return errs; +} + +static int v_numeric_float(const celix_jansson_schema_node_t* n, + json_t* inst, + celix_jansson_path_t* p, + celix_jansson_validation_context_t* ctx) { + /* only dispatched from type_slots[6] (JSON_REAL) or the aliased slot 5 for JSON_INTEGER */ + assert(json_is_real(inst) || json_is_integer(inst)); + double v = json_is_real(inst) ? json_real_value(inst) : (double)json_integer_value(inst); + int errs = 0; + if (n->u.numeric.has_min) { + if (n->u.numeric.exclusive_min ? (v <= n->u.numeric.bounds.f.min) : (v < n->u.numeric.bounds.f.min)) { + emit_error(ctx, p, "instance is below minimum of %.16g", n->u.numeric.bounds.f.min); + errs++; + } + } + if (n->u.numeric.has_max) { + if (n->u.numeric.exclusive_max ? (v >= n->u.numeric.bounds.f.max) : (v > n->u.numeric.bounds.f.max)) { + emit_error(ctx, p, "instance exceeds maximum of %.16g", n->u.numeric.bounds.f.max); + errs++; + } + } + if (n->u.numeric.has_mult) { + if (violates_multiple(v, n->u.numeric.multiple_of)) { + emit_error(ctx, p, "instance is not a multiple of %g", n->u.numeric.multiple_of); + errs++; + } + } + return errs; +} +static const schema_vtable vt_numeric_int = {v_numeric_int, NULL, d_numeric}; +static const schema_vtable vt_numeric_float = {v_numeric_float, NULL, d_numeric}; + +/* -- object -- */ +static void obj_validate_deps(const celix_jansson_schema_node_t* n, + json_t* inst, + celix_jansson_path_t* p, + celix_jansson_validation_context_t* ctx, + int* errs); + +static int v_object(const celix_jansson_schema_node_t* n, + json_t* inst, + celix_jansson_path_t* p, + celix_jansson_validation_context_t* ctx) { + assert(json_is_object(inst)); /* only dispatched from type_slots[1] (JSON_OBJECT) */ + int errs = 0; + size_t sz = json_object_size(inst); + + if (n->u.object.has_min_p && sz < n->u.object.min_p) { + emit_error(ctx, p, "instance has too few properties (%zu < %zu)", sz, n->u.object.min_p); + errs++; + } + if (n->u.object.has_max_p && sz > n->u.object.max_p) { + emit_error(ctx, p, "instance has too many properties (%zu > %zu)", sz, n->u.object.max_p); + errs++; + } + + /* required check */ + for (size_t i = 0; i < n->u.object.required_len; i++) { + if (!json_object_get(inst, n->u.object.required[i])) { + emit_error(ctx, p, "required property '%s' not found in object", n->u.object.required[i]); + errs++; + if (ctx->aborted) return errs; + } + } + + /* per-property validation */ + const char* key; + json_t* val; + json_object_foreach(inst, key, val) { + if (ctx->aborted) break; + + /* propertyNames */ + if (n->u.object.property_names) { + json_t* kname = json_string(key); + if (!kname) { + /* Fail closed on OOM: stop the validation pass */ + emit_error(ctx, p, "out of memory while validating property name '%s'", key); + errs++; + return errs; + } + celix_auto(celix_jansson_path_t) kp = {0}; + if (!path_child_checked(&kp, p, key, ctx, &errs)) { + json_decref(kname); + return errs; + } + errs += n->u.object.property_names->vtable->validate(n->u.object.property_names, kname, &kp, ctx); + json_decref(kname); + if (ctx->aborted) break; + } + + celix_auto(celix_jansson_path_t) cp = {0}; + if (!path_child_checked(&cp, p, key, ctx, &errs)) { + return errs; + } + + bool matched = false; + + /* properties */ + celix_jansson_schema_node_t* prop = obj_node_get(n->u.object.properties, key); + if (prop) { + matched = true; + errs += prop->vtable->validate(prop, val, &cp, ctx); + } + + /* patternProperties */ + for (size_t j = 0; j < n->u.object.pp_len; j++) { + if (ctx->aborted) break; + if (regexec(&n->u.object.pattern_properties[j].re, key, 0, NULL, 0) == 0) { + matched = true; + errs += n->u.object.pattern_properties[j].sch->vtable->validate( + n->u.object.pattern_properties[j].sch, val, &cp, ctx); + } + } + + /* additionalProperties */ + if (!matched && n->u.object.additional_properties) { + first_sink_t* fs = first_sink_new(); + if (!fs) { + /* Fail closed: the additional-property check cannot be + * evaluated, so stop the validation pass */ + emit_error(ctx, &cp, "out of memory while validating additional property '%s'", key); + errs++; + return errs; + } + celix_jansson_validation_context_t fctx = *ctx; + fctx.sink = &fs->base; + n->u.object.additional_properties->vtable->validate(n->u.object.additional_properties, val, &cp, &fctx); + if (fs->got) { + emit_error(ctx, &cp, "validation failed for additional property '%s': %s", key, fs->msg ? fs->msg : ""); + errs++; + if (ctx->aborted) { fs->base.destroy(&fs->base); break; } + } + fs->base.destroy(&fs->base); + } + } + + /* default values for missing properties */ + { + if (n->u.object.properties) { + CELIX_STRING_HASH_MAP_ITERATE(n->u.object.properties, iter) { + const char* pk = iter.key; + celix_jansson_schema_node_t* ps = (celix_jansson_schema_node_t*)iter.value.ptrValue; + if (!json_object_get(inst, pk)) { + const json_t* def = NULL; + if (ps->vtable && ps->vtable->default_value) { + celix_auto(celix_jansson_path_t) dp = {0}; + if (!path_child_checked(&dp, p, pk, ctx, &errs)) { + return errs; + } + def = ps->vtable->default_value(ps, &dp, inst, ctx); + } + if (!def) + def = ps->default_value; + if (def) { + /* Patch path: the property's RFC 6901-escaped path, + * built via the path API (a raw "%s/%s" + * concatenation would corrupt keys with '~' or '/'). */ + celix_auto(celix_jansson_path_t) dp = {0}; + if (!path_child_checked(&dp, p, pk, ctx, &errs)) { + return errs; + } + const char* dstr = celix_jansson_path_str(&dp); /* NULL on OOM */ + if (!dstr) { + emit_error(ctx, p, "out of memory while applying default value"); + errs++; + return errs; + } + if (celix_json_patch_add(ctx->patch, dstr, json_incref((json_t*)def)) != 0) { + /* Fail closed on OOM (the value ref is consumed by + * patch_add on failure, so nothing to release; dp + * is released automatically on scope exit) */ + emit_error(ctx, p, "out of memory while applying default value"); + errs++; + return errs; + } + } + } + } + } + } + + /* dependencies */ + obj_validate_deps(n, inst, p, ctx, &errs); + + return errs; +} + +static void obj_validate_deps(const celix_jansson_schema_node_t* n, + json_t* inst, + celix_jansson_path_t* p, + celix_jansson_validation_context_t* ctx, + int* errs) { + if (!n->u.object.dependencies || celix_stringHashMap_size(n->u.object.dependencies) == 0) + return; + + const char* key; + json_t* val; + json_object_foreach(inst, key, val) { + if (ctx->aborted) break; + celix_jansson_schema_node_t* dep = obj_node_get(n->u.object.dependencies, key); + if (dep) { + celix_auto(celix_jansson_path_t) cp = {0}; + if (!path_child_checked(&cp, p, key, ctx, errs)) { + return; + } + *errs += dep->vtable->validate(dep, inst, &cp, ctx); + } + } +} + +//LCOV_EXCL_START +static const json_t* dv_object(const celix_jansson_schema_node_t* n, + celix_jansson_path_t* p, + const json_t* inst, + celix_jansson_validation_context_t* ctx) { + (void)p; + (void)inst; + (void)ctx; + return n->default_value; +} +//LCOV_EXCL_STOP +static const schema_vtable vt_object = {v_object, dv_object, d_object}; + +/* -- array -- */ +static int v_array(const celix_jansson_schema_node_t* n, + json_t* inst, + celix_jansson_path_t* p, + celix_jansson_validation_context_t* ctx) { + assert(json_is_array(inst)); /* only dispatched from type_slots[2] (JSON_ARRAY) */ + size_t sz = json_array_size(inst); + int errs = 0; + + if (n->u.array.has_min_i && sz < n->u.array.min_items) { + emit_error(ctx, p, "instance has too few items (%zu < %zu)", sz, n->u.array.min_items); + errs++; + } + if (n->u.array.has_max_i && sz > n->u.array.max_items) { + emit_error(ctx, p, "instance has too many items (%zu > %zu)", sz, n->u.array.max_items); + errs++; + } + if (n->u.array.unique_items) { + for (size_t i = 0; i < sz; i++) { + for (size_t j = i + 1; j < sz; j++) { + if (jss_json_equal(json_array_get(inst, i), json_array_get(inst, j))) { + emit_error(ctx, p, "items have to be unique for this array"); + errs++; + goto unique_done; + } + } + } + unique_done:; + } + + /* items */ + if (n->u.array.items_schema) { + for (size_t i = 0; i < sz; i++) { + if (ctx->aborted) break; + char idx[32]; + snprintf(idx, sizeof(idx), "%zu", i); + celix_auto(celix_jansson_path_t) cp = {0}; + if (!path_child_checked(&cp, p, idx, ctx, &errs)) { + return errs; + } + errs += + n->u.array.items_schema->vtable->validate(n->u.array.items_schema, json_array_get(inst, i), &cp, ctx); + } + } else if (n->u.array.items_len > 0) { + /* tuple form */ + for (size_t i = 0; i < sz && i < n->u.array.items_len; i++) { + if (ctx->aborted) break; + char idx[32]; + snprintf(idx, sizeof(idx), "%zu", i); + celix_auto(celix_jansson_path_t) cp = {0}; + if (!path_child_checked(&cp, p, idx, ctx, &errs)) { + return errs; + } + errs += n->u.array.items[i]->vtable->validate(n->u.array.items[i], json_array_get(inst, i), &cp, ctx); + } + if (n->u.array.additional_items && sz > n->u.array.items_len) { + for (size_t i = n->u.array.items_len; i < sz; i++) { + if (ctx->aborted) break; + char idx[32]; + snprintf(idx, sizeof(idx), "%zu", i); + celix_auto(celix_jansson_path_t) cp = {0}; + if (!path_child_checked(&cp, p, idx, ctx, &errs)) { + return errs; + } + errs += n->u.array.additional_items->vtable->validate( + n->u.array.additional_items, json_array_get(inst, i), &cp, ctx); + } + } + } + + /* contains */ + if (n->u.array.contains) { + bool found = false; + for (size_t i = 0; i < sz && !found; i++) { + first_sink_t* fs = first_sink_new(); + if (!fs) { + /* Fail closed: 'contains' cannot be evaluated, so stop */ + emit_error(ctx, p, "out of memory while validating 'contains'"); + errs++; + return errs; + } + celix_jansson_validation_context_t fctx = *ctx; + fctx.sink = &fs->base; + char idx[32]; + snprintf(idx, sizeof(idx), "%zu", i); + celix_auto(celix_jansson_path_t) cp = {0}; + if (!path_child_checked(&cp, p, idx, ctx, &errs)) { + fs->base.destroy(&fs->base); + return errs; + } + int rc = n->u.array.contains->vtable->validate(n->u.array.contains, json_array_get(inst, i), &cp, &fctx); + if (rc == 0) + found = true; + fs->base.destroy(&fs->base); + } + if (!found) { + emit_error(ctx, p, "no element satisfies the 'contains' schema"); + errs++; + } + } + + return errs; +} +static const schema_vtable vt_array = {v_array, NULL, d_array}; + +/* -- not -- */ +static int v_not(const celix_jansson_schema_node_t* n, + json_t* inst, + celix_jansson_path_t* p, + celix_jansson_validation_context_t* ctx) { + first_sink_t* fs = first_sink_new(); + if (!fs) { + /* Fail closed: cannot determine whether the subschema validates */ + emit_error(ctx, p, "out of memory while validating 'not'"); + return 1; + } + celix_jansson_validation_context_t fctx = *ctx; + fctx.sink = &fs->base; + int sub_errs = n->u.not_schema.sub->vtable->validate(n->u.not_schema.sub, inst, p, &fctx); + fs->base.destroy(&fs->base); + if (sub_errs == 0) { + emit_error(ctx, p, "the subschema has succeeded, but it is required to not validate"); + return 1; + } + return 0; +} +static const schema_vtable vt_not = {v_not, NULL, d_not}; + +/* -- combination (allOf / anyOf / oneOf) -- */ +static int v_comb(const celix_jansson_schema_node_t* n, + json_t* inst, + celix_jansson_path_t* p, + celix_jansson_validation_context_t* ctx) { + size_t old_patch = json_array_size(ctx->patch); + int count = 0; + collecting_sink_t* master = coll_new(); + if (!master) { + /* Fail closed: return the same "combination failed, error emitted" + * convention as the other failing paths below (0 = passed) */ + emit_error(ctx, p, "out of memory while validating combination"); + return 1; + } + + for (size_t i = 0; i < n->u.combination.len; i++) { + collecting_sink_t* cs = coll_new(); + if (!cs) { + emit_error(ctx, p, "out of memory while validating combination"); + /* Flush the errors collected by the earlier branches, then clean up */ + coll_propagate(master, ctx->sink, NULL); + master->base.destroy(&master->base); + celix_json_patch_truncate(ctx->patch, old_patch); + return 1; + } + celix_jansson_validation_context_t cctx = *ctx; + cctx.sink = &cs->base; + + size_t branch_patch = json_array_size(ctx->patch); + int sub = n->u.combination.items[i]->vtable->validate(n->u.combination.items[i], inst, p, &cctx); + + if (sub == 0) + count++; + else + celix_json_patch_truncate(ctx->patch, branch_patch); + + char prefix[64]; + const char* kname = (n->kind == CELIX_JANSSON_SCHEMA_KIND_ALL_OF) ? "allOf" + : (n->kind == CELIX_JANSSON_SCHEMA_KIND_ANY_OF) ? "anyOf" + : "oneOf"; + snprintf(prefix, sizeof(prefix), "[combination: %s / case#%zu] ", kname, i); + coll_propagate(cs, &master->base, prefix); + cs->base.destroy(&cs->base); + } + + if (n->kind == CELIX_JANSSON_SCHEMA_KIND_ALL_OF) { + if (count < (int)n->u.combination.len) { + emit_error(ctx, + p, + "at least one subschema has failed, but all of them are required to validate - %zu failed", + n->u.combination.len - (size_t)count); + coll_propagate(master, ctx->sink, NULL); + master->base.destroy(&master->base); + celix_json_patch_truncate(ctx->patch, old_patch); + return 1; + } + } else if (n->kind == CELIX_JANSSON_SCHEMA_KIND_ANY_OF) { + if (count == 0) { + emit_error(ctx, + p, + "no subschema has succeeded, but one of them is required to validate. Type: anyOf, number of " + "failed subschemas: %zu", + n->u.combination.len); + coll_propagate(master, ctx->sink, NULL); + master->base.destroy(&master->base); + celix_json_patch_truncate(ctx->patch, old_patch); + return 1; + } + } else { /* ONE_OF */ + if (count == 0) { + emit_error(ctx, + p, + "no subschema has succeeded, but one of them is required to validate. Type: oneOf, number of " + "failed subschemas: %zu", + n->u.combination.len); + coll_propagate(master, ctx->sink, NULL); + master->base.destroy(&master->base); + celix_json_patch_truncate(ctx->patch, old_patch); + return 1; + } + if (count > 1) { + emit_error( + ctx, p, "more than one subschema has succeeded, but exactly one of them is required to validate"); + master->base.destroy(&master->base); + celix_json_patch_truncate(ctx->patch, old_patch); + return 1; + } + } + master->base.destroy(&master->base); + return 0; +} +static const schema_vtable vt_comb = {v_comb, NULL, d_comb}; + +/* -- type_schema (dispatcher) -- */ +static int v_type(const celix_jansson_schema_node_t* n, + json_t* inst, + celix_jansson_path_t* p, + celix_jansson_validation_context_t* ctx) { + int slot = celix_jansson_type_index(inst); + int errs = 0; + + /* Type check */ + celix_jansson_schema_node_t* typed = n->u.type_schema.type_slots[slot]; + if (!typed) { + emit_error(ctx, p, "unexpected instance type"); + return 1; + } + errs += typed->vtable->validate(typed, inst, p, ctx); + if (ctx->aborted) return errs; + + /* enum */ + if (n->u.type_schema.has_enum) { + bool found = false; + json_t* ev = n->u.type_schema.enum_values; + for (size_t i = 0; i < json_array_size(ev); i++) { + if (jss_json_equal(inst, json_array_get(ev, i))) { + found = true; + break; + } + } + if (!found) { + emit_error(ctx, p, "instance not found in required enum"); + errs++; + } + } + + /* const */ + if (n->u.type_schema.has_const) { + if (!jss_json_equal(inst, n->u.type_schema.const_value)) { + emit_error(ctx, p, "instance not const"); + errs++; + } + } + + /* logical combinators */ + for (size_t i = 0; i < n->u.type_schema.logic_len; i++) { + if (ctx->aborted) break; + errs += n->u.type_schema.logic[i]->vtable->validate(n->u.type_schema.logic[i], inst, p, ctx); + } + + /* if/then/else */ + if (n->u.type_schema.if_schema) { + first_sink_t* fs = first_sink_new(); + if (!fs) { + /* Fail closed: the if-condition cannot be evaluated */ + emit_error(ctx, p, "out of memory while evaluating 'if'"); + errs++; + return errs; + } + celix_jansson_validation_context_t fctx = *ctx; + fctx.sink = &fs->base; + int if_errs = n->u.type_schema.if_schema->vtable->validate(n->u.type_schema.if_schema, inst, p, &fctx); + fs->base.destroy(&fs->base); + + if (if_errs == 0) { + if (n->u.type_schema.then_schema) { + errs += n->u.type_schema.then_schema->vtable->validate(n->u.type_schema.then_schema, inst, p, ctx); + if (ctx->aborted) return errs; + } + } else { + if (n->u.type_schema.else_schema) { + errs += n->u.type_schema.else_schema->vtable->validate(n->u.type_schema.else_schema, inst, p, ctx); + if (ctx->aborted) return errs; + } + } + } + + /* Root default: null instance gets default_value */ + if (json_is_null(inst) && n->default_value) { + /* OOM: path_str returns NULL only on allocation failure; guard before + * json_incref (leak) and before patch_add (crash on NULL path) */ + const char* ps = celix_jansson_path_str(p); + if (!ps) { + emit_error(ctx, p, "out of memory while applying default value"); + errs++; + return errs; + } + if (celix_json_patch_add(ctx->patch, ps, json_incref(n->default_value)) != 0) { + /* Fail closed on OOM (the value ref is consumed by patch_add on + * failure, so nothing to release) */ + emit_error(ctx, p, "out of memory while applying default value"); + errs++; + return errs; + } + } + + return errs; +} +static const json_t* dv_type(const celix_jansson_schema_node_t* n, + celix_jansson_path_t* p, + const json_t* inst, + celix_jansson_validation_context_t* ctx) { + (void)p; + (void)inst; + (void)ctx; + return n->default_value; +} +static const schema_vtable vt_type = {v_type, dv_type, d_type}; + +/* ════════════════════════════════════════════════════════════════════════ + * celix_jansson_schema_make — the compiler + * ════════════════════════════════════════════════════════════════════════ */ + +/* Forward declarations for functions defined later in this file */ +static celix_jansson_schema_node_t* resolve_document_fragment( + celix_jansson_schema_root_t* root, const char* location, const char* fragment, + int depth); + +static int schema_make_internal_depth(json_t* sch, + celix_jansson_schema_root_t* root, + const celix_jansson_uri_t* base, + celix_jansson_uri_t* eff_base_out, + celix_jansson_schema_node_t** out, + int depth); +static int schema_make_internal(json_t* sch, celix_jansson_schema_root_t* root, + const celix_jansson_uri_t* base, + celix_jansson_schema_node_t** out, + int depth) { + return schema_make_internal_depth(sch, root, base, NULL, out, depth); +} + +static int make_type_schema(json_t* sch, celix_jansson_schema_root_t* root, + const celix_jansson_uri_t* base, + celix_jansson_schema_node_t** out, + int depth) { + celix_autoptr(celix_jansson_schema_node_t) n = (celix_jansson_schema_node_t*)calloc(1, sizeof(*n)); + if (!n) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + n->vtable = &vt_type; + n->kind = CELIX_JANSSON_SCHEMA_KIND_TYPE; + n->root = root; + n->refcount = 1; + + /* Parse "type" keyword */ + json_t* type_val = json_object_get(sch, "type"); + if (type_val) { + if (json_is_string(type_val)) { + const char* tname = json_string_value(type_val); + int slot = -1; + if (strcmp(tname, "null") == 0) + slot = 0; + else if (strcmp(tname, "object") == 0) + slot = 1; + else if (strcmp(tname, "array") == 0) + slot = 2; + else if (strcmp(tname, "string") == 0) + slot = 3; + else if (strcmp(tname, "boolean") == 0) + slot = 4; + else if (strcmp(tname, "integer") == 0) + slot = 5; + else if (strcmp(tname, "number") == 0) + slot = 6; + if (slot >= 0) { + /* Create per-type validator */ + celix_autoptr(celix_jansson_schema_node_t) typed = (celix_jansson_schema_node_t*)calloc(1, sizeof(*n)); + if (!typed) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd (d_type unrefs filled slots) */ + typed->root = root; + typed->refcount = 1; + switch (slot) { + case 0: + typed->vtable = &vt_null; + typed->kind = CELIX_JANSSON_SCHEMA_KIND_NULL; + break; + case 1: + typed->vtable = &vt_object; + typed->kind = CELIX_JANSSON_SCHEMA_KIND_OBJECT; + break; + case 2: + typed->vtable = &vt_array; + typed->kind = CELIX_JANSSON_SCHEMA_KIND_ARRAY; + break; + case 3: + typed->vtable = &vt_string; + typed->kind = CELIX_JANSSON_SCHEMA_KIND_STRING; + break; + case 4: + typed->vtable = &vt_booltype; + typed->kind = CELIX_JANSSON_SCHEMA_KIND_BOOLEAN_TYPE; + break; + case 5: + typed->vtable = &vt_numeric_int; + typed->kind = CELIX_JANSSON_SCHEMA_KIND_NUMERIC_INT; + break; + case 6: + typed->vtable = &vt_numeric_float; + typed->kind = CELIX_JANSSON_SCHEMA_KIND_NUMERIC_FLOAT; + break; + } + n->u.type_schema.type_slots[slot] = celix_steal_ptr(typed); + } + } else if (json_is_array(type_val)) { + for (size_t i = 0; i < json_array_size(type_val); i++) { + const char* tname = json_string_value(json_array_get(type_val, i)); + int slot = -1; + if (!tname) + continue; + if (strcmp(tname, "null") == 0) + slot = 0; + else if (strcmp(tname, "object") == 0) + slot = 1; + else if (strcmp(tname, "array") == 0) + slot = 2; + else if (strcmp(tname, "string") == 0) + slot = 3; + else if (strcmp(tname, "boolean") == 0) + slot = 4; + else if (strcmp(tname, "integer") == 0) + slot = 5; + else if (strcmp(tname, "number") == 0) + slot = 6; + if (slot >= 0) { + celix_autoptr(celix_jansson_schema_node_t) typed = (celix_jansson_schema_node_t*)calloc(1, sizeof(*n)); + if (!typed) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd (d_type unrefs filled slots) */ + typed->root = root; + typed->refcount = 1; + switch (slot) { + case 0: + typed->vtable = &vt_null; + typed->kind = CELIX_JANSSON_SCHEMA_KIND_NULL; + break; + case 1: + typed->vtable = &vt_object; + typed->kind = CELIX_JANSSON_SCHEMA_KIND_OBJECT; + break; + case 2: + typed->vtable = &vt_array; + typed->kind = CELIX_JANSSON_SCHEMA_KIND_ARRAY; + break; + case 3: + typed->vtable = &vt_string; + typed->kind = CELIX_JANSSON_SCHEMA_KIND_STRING; + break; + case 4: + typed->vtable = &vt_booltype; + typed->kind = CELIX_JANSSON_SCHEMA_KIND_BOOLEAN_TYPE; + break; + case 5: + typed->vtable = &vt_numeric_int; + typed->kind = CELIX_JANSSON_SCHEMA_KIND_NUMERIC_INT; + break; + case 6: + typed->vtable = &vt_numeric_float; + typed->kind = CELIX_JANSSON_SCHEMA_KIND_NUMERIC_FLOAT; + break; + } + n->u.type_schema.type_slots[slot] = celix_steal_ptr(typed); + } + } + } + } else { + /* No type specified — all slots */ + for (int slot = 0; slot < 7; slot++) { + celix_autoptr(celix_jansson_schema_node_t) typed = (celix_jansson_schema_node_t*)calloc(1, sizeof(*n)); + if (!typed) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd (d_type unrefs filled slots) */ + typed->root = root; + typed->refcount = 1; + static const schema_vtable* slots[] = { + &vt_null, &vt_object, &vt_array, &vt_string, &vt_booltype, &vt_numeric_int, &vt_numeric_float}; + typed->vtable = slots[slot]; + static const enum celix_jansson_schema_kind_e kinds[] = {CELIX_JANSSON_SCHEMA_KIND_NULL, + CELIX_JANSSON_SCHEMA_KIND_OBJECT, + CELIX_JANSSON_SCHEMA_KIND_ARRAY, + CELIX_JANSSON_SCHEMA_KIND_STRING, + CELIX_JANSSON_SCHEMA_KIND_BOOLEAN_TYPE, + CELIX_JANSSON_SCHEMA_KIND_NUMERIC_INT, + CELIX_JANSSON_SCHEMA_KIND_NUMERIC_FLOAT}; + typed->kind = kinds[slot]; + n->u.type_schema.type_slots[slot] = celix_steal_ptr(typed); + } + } + + /* alias: number also validates integer */ + if (n->u.type_schema.type_slots[6] && !n->u.type_schema.type_slots[5]) + n->u.type_schema.type_slots[5] = celix_jansson_schema_ref(n->u.type_schema.type_slots[6]); + + /* Parse string constraints */ + { + celix_jansson_schema_node_t* snode = n->u.type_schema.type_slots[3]; /* string slot */ + if (snode) { + json_t* v; + if ((v = json_object_get(sch, "minLength"))) { + snode->u.string.has_min_len = true; + snode->u.string.min_len = (size_t)json_integer_value(v); + } + if ((v = json_object_get(sch, "maxLength"))) { + snode->u.string.has_max_len = true; + snode->u.string.max_len = (size_t)json_integer_value(v); + } + if ((v = json_object_get(sch, "pattern"))) { + const char* ps = json_string_value(v); + snode->u.string.pattern_str = strdup(ps); + if (!snode->u.string.pattern_str) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd */ + if (regcomp(&snode->u.string.pattern, ps, REG_EXTENDED) != 0) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_PATTERN; /* has_pattern still false, so d_str skips regfree */ + snode->u.string.has_pattern = true; + } + if ((v = json_object_get(sch, "format"))) { + snode->u.string.format = strdup(json_string_value(v)); + if (!snode->u.string.format) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd */ + snode->u.string.has_format = true; + if (!root->format) + return CELIX_JANSSON_SCHEMA_ERROR_FORMAT_CHECKER; /* n auto-unref'd */ + } + if ((v = json_object_get(sch, "contentEncoding"))) { + snode->u.string.content_encoding = strdup(json_string_value(v)); + if (!snode->u.string.content_encoding) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd */ + snode->u.string.has_content = true; + if (!root->content) + return CELIX_JANSSON_SCHEMA_ERROR_CONTENT_CHECKER; /* n auto-unref'd */ + } + if ((v = json_object_get(sch, "contentMediaType"))) { + snode->u.string.has_content = true; + snode->u.string.content_media_type = strdup(json_string_value(v)); + } + } + } + + /* Parse numeric constraints (apply to both int and float slots) */ + { + json_t* v; + for (int ns = 5; ns <= 6; ns++) { + celix_jansson_schema_node_t* nnum = n->u.type_schema.type_slots[ns]; + if (!nnum) + continue; + if ((v = json_object_get(sch, "minimum"))) { + nnum->u.numeric.has_min = true; + if (ns == 5) + nnum->u.numeric.bounds.i.min = json_integer_value(v); + else + nnum->u.numeric.bounds.f.min = json_is_real(v) ? json_real_value(v) : (double)json_integer_value(v); + } + if ((v = json_object_get(sch, "maximum"))) { + nnum->u.numeric.has_max = true; + if (ns == 5) + nnum->u.numeric.bounds.i.max = json_integer_value(v); + else + nnum->u.numeric.bounds.f.max = json_is_real(v) ? json_real_value(v) : (double)json_integer_value(v); + } + if ((v = json_object_get(sch, "exclusiveMinimum"))) { + nnum->u.numeric.exclusive_min = true; + nnum->u.numeric.has_min = true; + if (ns == 5) + nnum->u.numeric.bounds.i.min = json_integer_value(v); + else + nnum->u.numeric.bounds.f.min = json_is_real(v) ? json_real_value(v) : (double)json_integer_value(v); + } + if ((v = json_object_get(sch, "exclusiveMaximum"))) { + nnum->u.numeric.exclusive_max = true; + nnum->u.numeric.has_max = true; + if (ns == 5) + nnum->u.numeric.bounds.i.max = json_integer_value(v); + else + nnum->u.numeric.bounds.f.max = json_is_real(v) ? json_real_value(v) : (double)json_integer_value(v); + } + if ((v = json_object_get(sch, "multipleOf"))) { + nnum->u.numeric.has_mult = true; + nnum->u.numeric.multiple_of = json_is_real(v) ? json_real_value(v) : (double)json_integer_value(v); + } + } + } + + /* Parse object constraints */ + { + celix_jansson_schema_node_t* onode = n->u.type_schema.type_slots[1]; + if (onode) { + json_t* v; + if ((v = json_object_get(sch, "minProperties"))) { + onode->u.object.has_min_p = true; + onode->u.object.min_p = (size_t)json_integer_value(v); + } + if ((v = json_object_get(sch, "maxProperties"))) { + onode->u.object.has_max_p = true; + onode->u.object.max_p = (size_t)json_integer_value(v); + } + if ((v = json_object_get(sch, "required"))) { + size_t rlen = json_array_size(v); + onode->u.object.required = (char**)calloc(rlen, sizeof(char*)); + /* Check before setting required_len (d_object loops over required_len) */ + if (rlen > 0 && !onode->u.object.required) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd */ + onode->u.object.required_len = rlen; + for (size_t i = 0; i < rlen; i++) { + const char* rname = json_string_value(json_array_get(v, i)); + if (!rname) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_SCHEMA; /* non-string entry; n auto-unref'd */ + onode->u.object.required[i] = strdup(rname); + if (!onode->u.object.required[i]) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd (d_object frees NULL tails) */ + } + } + if ((v = json_object_get(sch, "properties"))) { + onode->u.object.properties = obj_node_map_create(); + if (!onode->u.object.properties) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd */ + const char* k; + json_t* pv; + json_object_foreach(v, k, pv) { + celix_jansson_schema_node_t* ps = NULL; + int rc = schema_make_internal(pv, root, base, &ps, depth + 1); + if (rc != CELIX_JANSSON_SCHEMA_OK) + return rc; /* n auto-unref'd (d_object destroys the map and subschemas already inserted) */ + if (celix_stringHashMap_put(onode->u.object.properties, k, ps) != 0) { + celix_jansson_schema_unref(ps); /* the map did not take the ref */ + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd */ + } + } + } + if ((v = json_object_get(sch, "patternProperties"))) { + size_t ppc = json_object_size(v); + onode->u.object.pattern_properties = (typeof(onode->u.object.pattern_properties))calloc( + ppc, sizeof(*onode->u.object.pattern_properties)); + /* Check before setting pp_len (d_object loops over pp_len) */ + if (ppc > 0 && !onode->u.object.pattern_properties) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd */ + onode->u.object.pp_len = ppc; + const char* pk; + json_t* psch; + size_t idx = 0; + json_object_foreach(v, pk, psch) { + onode->u.object.pattern_properties[idx].sch = NULL; + if (regcomp(&onode->u.object.pattern_properties[idx].re, pk, REG_EXTENDED) != 0) { + /* The failed regcomp leaves the regex_t at idx undefined, + * so only regfree the entries compiled so far (0..idx-1); + * reset pp_len so d_object skips the never-compiled tail */ + for (size_t k = 0; k < idx; k++) { + regfree(&onode->u.object.pattern_properties[k].re); + celix_jansson_schema_unref(onode->u.object.pattern_properties[k].sch); + } + free(onode->u.object.pattern_properties); + onode->u.object.pattern_properties = NULL; + onode->u.object.pp_len = 0; + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_PATTERN; + } + int rc = schema_make_internal(psch, root, base, + &onode->u.object.pattern_properties[idx].sch, depth + 1); + if (rc != CELIX_JANSSON_SCHEMA_OK) { + /* Manually release only the regcomp'd entries (0..idx) and reset + * pp_len so d_object skips the never-compiled tail on n's auto-unref */ + for (size_t k = 0; k <= idx; k++) { + regfree(&onode->u.object.pattern_properties[k].re); + celix_jansson_schema_unref(onode->u.object.pattern_properties[k].sch); + } + free(onode->u.object.pattern_properties); + onode->u.object.pattern_properties = NULL; + onode->u.object.pp_len = 0; + return rc; + } + idx++; + } + } + if ((v = json_object_get(sch, "additionalProperties"))) { + if (json_is_object(v) || json_is_boolean(v)) { + int rc = schema_make_internal(v, root, base, &onode->u.object.additional_properties, depth + 1); + if (rc != CELIX_JANSSON_SCHEMA_OK) + return rc; /* n auto-unref'd */ + } + } + if ((v = json_object_get(sch, "dependencies"))) { + onode->u.object.dependencies = obj_node_map_create(); + if (!onode->u.object.dependencies) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd */ + const char* dk; + json_t* dv; + json_object_foreach(v, dk, dv) { + celix_jansson_schema_node_t* dep = NULL; + if (json_is_array(dv)) { + /* Array form → required list */ + celix_autoptr(celix_jansson_schema_node_t) rn = (celix_jansson_schema_node_t*)calloc(1, sizeof(*rn)); + if (!rn) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd */ + rn->vtable = &vt_required; + rn->kind = CELIX_JANSSON_SCHEMA_KIND_REQUIRED; + rn->root = root; + rn->refcount = 1; + size_t alen = json_array_size(dv); + rn->u.required.names = (char**)calloc(alen, sizeof(char*)); + /* Check before setting len; on failure rn auto-unrefs via d_required + * (len=0, names=NULL → loop 0, free(NULL), free(rn)) */ + if (alen > 0 && !rn->u.required.names) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + rn->u.required.len = alen; + for (size_t ai = 0; ai < alen; ai++) { + const char* dname = json_string_value(json_array_get(dv, ai)); + if (!dname) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_SCHEMA; /* non-string entry; n and rn auto-unref'd */ + rn->u.required.names[ai] = strdup(dname); + if (!rn->u.required.names[ai]) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n and rn auto-unref'd */ + } + dep = celix_steal_ptr(rn); + } else { + int rc = schema_make_internal(dv, root, base, &dep, depth + 1); + if (rc != CELIX_JANSSON_SCHEMA_OK) + return rc; /* n auto-unref'd */ + } + if (dep) { + if (celix_stringHashMap_put(onode->u.object.dependencies, dk, dep) != 0) { + celix_jansson_schema_unref(dep); /* the map did not take the ref */ + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd */ + } + } + } + } + if ((v = json_object_get(sch, "propertyNames"))) { + int rc = schema_make_internal(v, root, base, &onode->u.object.property_names, depth + 1); + if (rc != CELIX_JANSSON_SCHEMA_OK) + return rc; /* n auto-unref'd */ + } + } + } + + /* Parse array constraints */ + { + celix_jansson_schema_node_t* anode = n->u.type_schema.type_slots[2]; + if (anode) { + json_t* v; + if ((v = json_object_get(sch, "minItems"))) { + anode->u.array.has_min_i = true; + anode->u.array.min_items = (size_t)json_integer_value(v); + } + if ((v = json_object_get(sch, "maxItems"))) { + anode->u.array.has_max_i = true; + anode->u.array.max_items = (size_t)json_integer_value(v); + } + if ((v = json_object_get(sch, "uniqueItems"))) + anode->u.array.unique_items = json_is_true(v); + if ((v = json_object_get(sch, "items"))) { + if (json_is_object(v) || json_is_boolean(v)) { + int rc = schema_make_internal(v, root, base, &anode->u.array.items_schema, depth + 1); + if (rc != CELIX_JANSSON_SCHEMA_OK) + return rc; /* n auto-unref'd */ + } else if (json_is_array(v)) { + size_t ilen = json_array_size(v); + anode->u.array.items = + (celix_jansson_schema_node_t**)calloc(ilen, sizeof(celix_jansson_schema_node_t*)); + /* Check before setting items_len (d_array loops over items_len) */ + if (ilen > 0 && !anode->u.array.items) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd */ + anode->u.array.items_len = ilen; + for (size_t i = 0; i < ilen; i++) { + int rc = schema_make_internal(json_array_get(v, i), root, base, + &anode->u.array.items[i], depth + 1); + if (rc != CELIX_JANSSON_SCHEMA_OK) { + /* Manually release the compiled entries (0..i-1) and reset + * items_len so d_array skips the never-compiled tail on + * n's auto-unref */ + for (size_t k = 0; k < i; k++) + celix_jansson_schema_unref(anode->u.array.items[k]); + free(anode->u.array.items); + anode->u.array.items = NULL; + anode->u.array.items_len = 0; + return rc; + } + } + } + } + if ((v = json_object_get(sch, "additionalItems"))) { + int rc = schema_make_internal(v, root, base, &anode->u.array.additional_items, depth + 1); + if (rc != CELIX_JANSSON_SCHEMA_OK) + return rc; /* n auto-unref'd */ + } + if ((v = json_object_get(sch, "contains"))) { + int rc = schema_make_internal(v, root, base, &anode->u.array.contains, depth + 1); + if (rc != CELIX_JANSSON_SCHEMA_OK) + return rc; /* n auto-unref'd */ + } + } + } + + /* enum */ + { + json_t* v = json_object_get(sch, "enum"); + if (v) { + n->u.type_schema.enum_values = json_deep_copy(v); + if (!n->u.type_schema.enum_values) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd */ + n->u.type_schema.has_enum = true; + } + } + + /* const */ + { + json_t* v = json_object_get(sch, "const"); + if (v) { + n->u.type_schema.const_value = json_deep_copy(v); + if (!n->u.type_schema.const_value) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd */ + n->u.type_schema.has_const = true; + } + } + + /* logic combinators: not, allOf, anyOf, oneOf */ + { + json_t* v; + celix_jansson_vec_t logic; + celix_jansson_vec_init(&logic); + if ((v = json_object_get(sch, "not"))) { + celix_autoptr(celix_jansson_schema_node_t) sub = NULL; + int rc = schema_make_internal(v, root, base, &sub, depth + 1); + if (rc != CELIX_JANSSON_SCHEMA_OK) { + celix_jansson_vec_free(&logic); + return rc; /* n and sub auto-unref'd */ + } + celix_autoptr(celix_jansson_schema_node_t) nn = (celix_jansson_schema_node_t*)calloc(1, sizeof(*nn)); + if (!nn) { + celix_jansson_vec_free(&logic); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n and sub auto-unref'd */ + } + nn->vtable = &vt_not; + nn->kind = CELIX_JANSSON_SCHEMA_KIND_NOT; + nn->root = root; + nn->refcount = 1; + nn->u.not_schema.sub = celix_steal_ptr(sub); + celix_jansson_schema_node_t* stolen = celix_steal_ptr(nn); + if (celix_jansson_vec_push(&logic, stolen) != 0) { + /* The vec did not take the ref: release the node and the + * already-collected logic entries, then propagate NOMEM */ + celix_jansson_schema_unref(stolen); + //LCOV_EXCL_START: unreachable — the not node is always the first + //vec_push, so the logic vec is empty at this point (defensive loop) + for (size_t k = 0; k < logic.len; k++) + celix_jansson_schema_unref((celix_jansson_schema_node_t*)logic.items[k]); + //LCOV_EXCL_STOP + celix_jansson_vec_free(&logic); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd */ + } + } + static const char* combos[] = {"allOf", "anyOf", "oneOf"}; + static const enum celix_jansson_schema_kind_e ckinds[] = { + CELIX_JANSSON_SCHEMA_KIND_ALL_OF, CELIX_JANSSON_SCHEMA_KIND_ANY_OF, CELIX_JANSSON_SCHEMA_KIND_ONE_OF}; + for (int ci = 0; ci < 3; ci++) { + if ((v = json_object_get(sch, combos[ci]))) { + size_t clen = json_array_size(v); + celix_autoptr(celix_jansson_schema_node_t) cn = (celix_jansson_schema_node_t*)calloc(1, sizeof(*cn)); + if (!cn) { + /* logic may already hold a not node or earlier combos */ + for (size_t k = 0; k < logic.len; k++) + celix_jansson_schema_unref((celix_jansson_schema_node_t*)logic.items[k]); + celix_jansson_vec_free(&logic); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd */ + } + cn->vtable = &vt_comb; + cn->kind = ckinds[ci]; + cn->root = root; + cn->refcount = 1; + cn->u.combination.items = + (celix_jansson_schema_node_t**)calloc(clen, sizeof(celix_jansson_schema_node_t*)); + /* Check before setting len; on failure cn auto-unrefs via d_comb + * (len=0, items=NULL → loop 0, free(NULL), free(cn)) */ + if (clen > 0 && !cn->u.combination.items) { + for (size_t k = 0; k < logic.len; k++) + celix_jansson_schema_unref((celix_jansson_schema_node_t*)logic.items[k]); + celix_jansson_vec_free(&logic); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + cn->u.combination.len = clen; + for (size_t j = 0; j < clen; j++) { + int rc = schema_make_internal(json_array_get(v, j), root, base, + &cn->u.combination.items[j], depth + 1); + if (rc != CELIX_JANSSON_SCHEMA_OK) { + /* Propagate schema compile errors. cn auto-unrefs via d_comb: + * len is already clen and items[j..clen-1] are NULL from calloc + * (unref is NULL-safe), d_comb frees the array and cn. */ + /* vec_free only frees the array, not the elements: unref the + * not/earlier-combo nodes pushed into logic so far */ + for (size_t k = 0; k < logic.len; k++) + celix_jansson_schema_unref((celix_jansson_schema_node_t*)logic.items[k]); + celix_jansson_vec_free(&logic); + return rc; /* n and cn auto-unref'd */ + } + } + celix_jansson_schema_node_t* stolen = celix_steal_ptr(cn); + if (celix_jansson_vec_push(&logic, stolen) != 0) { + /* The vec did not take the ref: release the node and the + * already-collected logic entries, then propagate NOMEM */ + celix_jansson_schema_unref(stolen); + //LCOV_EXCL_START: unreachable — the vec grows 0→4 on the first + //push and holds at most 4 entries (not + 3 combos), so a combo + //push can only fail as the first push, with the vec still empty + for (size_t k = 0; k < logic.len; k++) + celix_jansson_schema_unref((celix_jansson_schema_node_t*)logic.items[k]); + //LCOV_EXCL_STOP + celix_jansson_vec_free(&logic); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd */ + } + } + } + if (logic.len > 0) { + n->u.type_schema.logic = (celix_jansson_schema_node_t**)logic.items; + n->u.type_schema.logic_len = logic.len; + } + } + + /* if/then/else */ + { + json_t* ifv = json_object_get(sch, "if"); + if (ifv) { + int rc = schema_make_internal(ifv, root, base, &n->u.type_schema.if_schema, depth + 1); + if (rc != CELIX_JANSSON_SCHEMA_OK) + return rc; /* n auto-unref'd */ + json_t* thenv = json_object_get(sch, "then"); + if (thenv) { + rc = schema_make_internal(thenv, root, base, &n->u.type_schema.then_schema, depth + 1); + if (rc != CELIX_JANSSON_SCHEMA_OK) + return rc; /* n auto-unref'd */ + } + json_t* elsev = json_object_get(sch, "else"); + if (elsev) { + rc = schema_make_internal(elsev, root, base, &n->u.type_schema.else_schema, depth + 1); + if (rc != CELIX_JANSSON_SCHEMA_OK) + return rc; /* n auto-unref'd */ + } + } + } + + /* default */ + { + json_t* dv = json_object_get(sch, "default"); + if (dv) { + n->default_value = json_deep_copy(dv); + if (!n->default_value) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* n auto-unref'd */ + } + } + + *out = celix_steal_ptr(n); + return CELIX_JANSSON_SCHEMA_OK; +} + +static int schema_make_internal_depth(json_t* sch, + celix_jansson_schema_root_t* root, + const celix_jansson_uri_t* base, + celix_jansson_uri_t* eff_base_out, + celix_jansson_schema_node_t** out, + int depth) { + //LCOV_EXCL_START: defensive — all call sites pass non-NULL arguments + if (!sch || !root || !out) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + //LCOV_EXCL_STOP + /* Guard against infinite recursion for self-referencing schemas */ + if (depth > 20) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_SCHEMA; + + /* ── Parse $id to compute effective base URI ─────────────────────── */ + const celix_jansson_uri_t* effective_base = base; + celix_auto(celix_jansson_uri_t) my_base = {0}; + bool has_id = false; + json_t* refv = json_object_get(sch, "$ref"); /* peek early: $id alongside $ref is not a real $id */ + json_t* idv = json_object_get(sch, "$id"); + if (idv && json_is_string(idv) && !refv) { + const celix_jansson_uri_t* derive_from = effective_base; + celix_auto(celix_jansson_uri_t) empty = {0}; + if (!derive_from) { + derive_from = ∅ + } + if (eff_base_out) { + /* Derive directly into caller's buffer — avoids double-free from struct copy */ + if (celix_jansson_uri_derive(derive_from, json_string_value(idv), eff_base_out) == 0) { + effective_base = eff_base_out; + has_id = true; + } + } else { + if (celix_jansson_uri_derive(derive_from, json_string_value(idv), &my_base) == 0) { + effective_base = &my_base; + has_id = true; + } + } + } + + if (json_is_boolean(sch)) + { + celix_autoptr(celix_jansson_schema_node_t) n = (celix_jansson_schema_node_t*)calloc(1, sizeof(*n)); + if (!n) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + n->vtable = &vt_boolean; + n->kind = CELIX_JANSSON_SCHEMA_KIND_BOOLEAN; + n->root = root; + n->refcount = 1; + n->u.boolean.value = json_boolean_value(sch); + *out = celix_steal_ptr(n); + /* No $id registration: a boolean schema cannot carry $id (json_object_get + * returns NULL for non-objects, so has_id is always false here) */ + return CELIX_JANSSON_SCHEMA_OK; + } + if (!json_is_object(sch)) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_SCHEMA; + + /* Process definitions first — compile and register using effective base location */ + json_t* defs = json_object_get(sch, "definitions"); + if (defs && json_is_object(defs)) { + char* base_loc = effective_base ? celix_jansson_uri_location(effective_base) : strdup(""); + if (!base_loc) { + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + celix_jansson_schema_file_t* sf = celix_jansson_schema_root_get_or_create_file(root, base_loc); + free(base_loc); + if (!sf) { + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + const char* dk; + json_t* dv; + json_object_foreach(defs, dk, dv) { + celix_jansson_schema_node_t* ds = NULL; + int rc = schema_make_internal(dv, root, effective_base, &ds, depth + 1); + if (rc != CELIX_JANSSON_SCHEMA_OK) { + return rc; + } + if (ds) { + celix_autofree char* frag = NULL; + if (asprintf(&frag, "/definitions/%s", dk) < 0) { + celix_jansson_schema_unref(ds); /* the map never took the ref */ + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + celix_jansson_schema_node_t* existing = + (celix_jansson_schema_node_t*)celix_stringHashMap_get(sf->schemas, frag); + if (existing) { + celix_jansson_schema_unref(ds); + } else { + if (celix_stringHashMap_put(sf->schemas, frag, ds) != 0) { + celix_jansson_schema_unref(ds); /* the map did not take the ref */ + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + } + } + } + } + + /* Check for $ref */ + if (refv) { + const char* ref_str = json_string_value(refv); + if (!ref_str) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_SCHEMA; + + /* Resolve ref_str against the effective base URI */ + celix_auto(celix_jansson_uri_t) ref_uri = {0}; + if (effective_base) { + if (celix_jansson_uri_derive(effective_base, ref_str, &ref_uri) != 0) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } else if (celix_jansson_uri_update(&ref_uri, ref_str) != 0) { + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* ref_uri cleared at scope exit */ + } + + char* rloc = celix_jansson_uri_location(&ref_uri); + char* rfra = celix_jansson_uri_fragment(&ref_uri); + if (!rloc || !rfra) { + free(rloc); + free(rfra); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + celix_jansson_schema_file_t* sf = celix_jansson_schema_root_get_or_create_file(root, rloc); + celix_jansson_schema_node_t* target = NULL; + bool plain_self_ref = (strcmp(ref_str, "#") == 0 && rloc[0] == '\0' && rfra[0] == '\0'); + if (sf) { + target = (celix_jansson_schema_node_t*)celix_stringHashMap_get(sf->schemas, rfra); + /* Document fragment walk for internal refs and external docs with document loaded */ + if (!target && !plain_self_ref && (sf->document || (rloc[0] == '\0' && root->original_schema))) { + target = resolve_document_fragment(root, rloc, rfra, depth + 1); + } + if (!target) { + target = (celix_jansson_schema_node_t*)celix_stringHashMap_get(sf->unresolved, rfra); + } + if (!target) { + /* Inner-scope autoptr: `target` itself must stay a plain pointer + * because it is also assigned borrowed pointers above (from + * sf->schemas / resolve_document_fragment / sf->unresolved) */ + celix_autoptr(celix_jansson_schema_node_t) t = (celix_jansson_schema_node_t*)calloc(1, sizeof(*t)); + if (t) { + t->vtable = &vt_ref; + t->kind = CELIX_JANSSON_SCHEMA_KIND_REF; + t->root = root; + t->refcount = 1; + char* uristr = celix_jansson_uri_to_string(&ref_uri); + if (!uristr) { + /* t is not yet in the unresolved map; auto-unref releases it */ + free(rloc); + free(rfra); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + t->u.ref.id = uristr; + /* Steal before the put: the map takes the caller's ref directly, + * and target must stay visible for the target_weak assignment below */ + target = celix_steal_ptr(t); + if (celix_stringHashMap_put(sf->unresolved, rfra, target) != 0) { + /* The map did not take the owning ref; target holds the only one */ + celix_jansson_schema_unref(target); + free(rloc); + free(rfra); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + } + } + } + + if (!target && !plain_self_ref) { + free(rloc); + free(rfra); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + + /* Create ref node — store derived URI as id */ + celix_autoptr(celix_jansson_schema_node_t) rn = (celix_jansson_schema_node_t*)calloc(1, sizeof(*rn)); + if (!rn) { + free(rloc); + free(rfra); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + rn->vtable = &vt_ref; + rn->kind = CELIX_JANSSON_SCHEMA_KIND_REF; + rn->root = root; + rn->refcount = 1; + rn->u.ref.id = celix_jansson_uri_to_string(&ref_uri); + if (!rn->u.ref.id) { + /* rn is not in any map and target_weak is still NULL; auto-unref via + * d_ref handles it (free(NULL), json_decref(NULL), free) */ + free(rloc); + free(rfra); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + if (target) { + rn->u.ref.target_weak = target; /* borrowed; the unresolved map holds the owning ref */ + } + + json_t* defv = json_object_get(sch, "default"); + if (defv) { + rn->default_value = json_deep_copy(defv); + if (!rn->default_value) { + /* rn auto-unref'd via d_ref (free(NULL), json_decref(NULL), free) */ + free(rloc); + free(rfra); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + } + + free(rloc); + free(rfra); + + /* No $id registration: draft-7 treats $id alongside $ref as not a real + * $id (JSON Reference: members other than $ref are ignored — see the + * !refv guard above), so has_id is always false here */ + *out = celix_steal_ptr(rn); + return CELIX_JANSSON_SCHEMA_OK; + } + + int rc = make_type_schema(sch, root, effective_base, out, depth); + if (rc == CELIX_JANSSON_SCHEMA_OK && has_id && *out) { + int ir = celix_jansson_schema_root_insert(root, &my_base, *out); + if (ir == CELIX_JANSSON_SCHEMA_ERROR_NOMEM) { + /* The registry did not ref *out; unref it so the caller does not + * take ownership on the error path */ + celix_jansson_schema_unref(*out); + *out = NULL; + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + /* DUPLICATE_URI is benign: with $id:"" the registration above already + * happened and set_root_schema's root_insert must tolerate it as well */ + } + return rc; +} + +/* ════════════════════════════════════════════════════════════════════════ + * Document fragment resolution with $id tracking + * ════════════════════════════════════════════════════════════════════════ */ + +/* Determine if a JSON pointer token points to a container whose values are + * schema objects (definitions, properties, patternProperties, dependencies, + * allOf, anyOf, oneOf, or the items-array tuple form). In such containers, + * $id inside the member values is a real schema identifier. */ +static bool token_members_are_schemas(const char* token, json_t* container) { + static const char* schema_containers[] = { + "definitions", "properties", "patternProperties", "dependencies", + "allOf", "anyOf", "oneOf", NULL + }; + for (const char** p = schema_containers; *p; p++) + if (strcmp(token, *p) == 0) + return true; + /* items: tuple form = array of schemas, single form = one schema */ + if (strcmp(token, "items") == 0 && json_is_array(container)) + return true; + return false; +} + +/* Determine if a token points to a schema-position node (i.e., the value + * at this key is itself a schema where $id would be meaningful). */ +static bool token_is_schema_position(const char* token, json_t* child) { + static const char* schema_positions[] = { + "additionalProperties", "additionalItems", "contains", + "propertyNames", "not", "if", "then", "else", NULL + }; + for (const char** p = schema_positions; *p; p++) + if (strcmp(token, *p) == 0) + return true; + /* "items" with a non-array value is a single schema */ + if (strcmp(token, "items") == 0 && !json_is_array(child)) + return true; + return false; +} + +/* Walk a JSON document following a JSON Pointer fragment, tracking $id + * base-URI changes along the path. Compile the target subtree and register + * it in the registry. + * + * Note: internal OOMs surface as NULL (indistinguishable from "fragment not + * found"), so callers report REF_UNRESOLVED/LOADER rather than NOMEM. That + * masking is accepted — the failure is still surfaced, and aborting the walk + * (instead of continuing with stale state) keeps the result trustworthy. */ +static celix_jansson_schema_node_t* resolve_document_fragment( + celix_jansson_schema_root_t* root, const char* location, const char* fragment, + int depth) +{ + celix_jansson_schema_file_t* sf = + (celix_jansson_schema_file_t*)celix_stringHashMap_get(root->files, location); + assert(sf != NULL); /* all callers guard on sf first */ + + json_t* doc = sf->document; + if (!doc && location[0] == '\0') + doc = root->original_schema; + if (!doc) + return NULL; + + /* Build the base URI for this walk */ + celix_auto(celix_jansson_uri_t) cur_base = {0}; + if (sf->base_uri) { + if (celix_jansson_uri_init(&cur_base, sf->base_uri) != 0) + return NULL; /* init leaves cur_base cleared */ + } else if (celix_jansson_uri_init(&cur_base, location[0] ? location : "") != 0) { + return NULL; + } + + /* Walk the fragment tokens */ + json_t* cur = doc; + bool members_are_schemas = false; + json_t* prev_container = NULL; + + if (fragment[0] == '/' && fragment[1] != '\0') { + /* Tokenize: split by '/' and decode ~0/~1 */ + const char* s = fragment + 1; /* skip leading '/' */ + const char* tok_start = s; + while (*tok_start) { + const char* tok_end = tok_start; + while (*tok_end && *tok_end != '/') + tok_end++; + + /* Decode token */ + celix_auto(celix_jansson_strbuf_t) tsb; + celix_jansson_strbuf_init(&tsb); + for (const char* c = tok_start; c < tok_end; c++) { + int rc; + if (*c == '~' && *(c+1) == '0') { rc = celix_jansson_strbuf_appendc(&tsb, '~'); c++; } + else if (*c == '~' && *(c+1) == '1') { rc = celix_jansson_strbuf_appendc(&tsb, '/'); c++; } + else rc = celix_jansson_strbuf_appendc(&tsb, *c); + if (rc != 0) { + /* OOM: abort the walk (see the "internal OOMs surface as + * NULL" note above) rather than continue with a truncated + * token; tsb is released automatically. */ + return NULL; + } + } + char* token = celix_jansson_strbuf_detach(&tsb); + if (!token) { + /* strbuf_detach also returns NULL for an empty buffer, so an + * empty token (double slash) is rejected here as well. */ + return NULL; + } + + prev_container = cur; + + if (!json_is_object(cur) && !json_is_array(cur)) { + free(token); + return NULL; + } + + json_t* child = NULL; + if (json_is_array(cur)) { + /* RFC 6901: array index token — must be a non-negative integer; + * an empty token never reaches this point (see detach above). */ + const char* t = token; + for (const char* c = t; *c; c++) { + if (*c < '0' || *c > '9') { + free(token); + return NULL; + } + } + size_t idx = (size_t)strtoul(t, NULL, 10); + child = (idx < json_array_size(cur)) ? json_array_get(cur, idx) : NULL; + } else { + child = json_object_get(cur, token); + } + + /* If entering a schema-position node with $id, update base. + * derive fails only on OOM: abort the walk rather than continuing + * with a stale base (which would resolve $refs to wrong targets) */ + if (child && json_is_object(child) && + (members_are_schemas || token_is_schema_position(token, child))) { + json_t* cid = json_object_get(child, "$id"); + if (cid && json_is_string(cid)) { + celix_jansson_uri_t new_base = {0}; + if (celix_jansson_uri_derive(&cur_base, json_string_value(cid), &new_base) != 0) { + free(token); + return NULL; + } + celix_jansson_uri_clear(&cur_base); + cur_base = new_base; + } + } + + members_are_schemas = token_members_are_schemas(token, child ? child : prev_container); + + if (!child) { + free(token); + return NULL; + } + cur = child; + free(token); + + tok_start = (*tok_end == '/') ? tok_end + 1 : tok_end; + } + } + + /* Compile the reached subtree with the tracked base */ + celix_jansson_schema_node_t* sch = NULL; + int rc = schema_make_internal_depth(cur, root, &cur_base, NULL, &sch, depth + 1); + if (rc != CELIX_JANSSON_SCHEMA_OK || !sch) + return NULL; + + /* Register under the full URI so waiting refs get resolved */ + celix_auto(celix_jansson_uri_t) full_uri = {0}; + if (celix_jansson_uri_init(&full_uri, location) != 0) { + /* init leaves full_uri cleared; sch is owned by this frame */ + celix_jansson_schema_unref(sch); + return NULL; + } + /* Re-attach fragment to the URI */ + if (fragment[0] == '/') { + celix_json_pointer_t ptr; + if (celix_json_pointer_init(&ptr, fragment) == 0) { + bool push_ok = true; + for (size_t i = 0; i < ptr.len; i++) { + if (celix_json_pointer_push(&full_uri.pointer, ptr.tokens[i]) != 0) { + push_ok = false; + break; + } + } + celix_json_pointer_clear(&ptr); + if (!push_ok) { + /* do not register under a partial fragment — it would wrongly + * satisfy a shorter-fragment waiting ref */ + celix_jansson_schema_unref(sch); + return NULL; + } + } + } + int ir = celix_jansson_schema_root_insert(root, &full_uri, sch); + if (ir == CELIX_JANSSON_SCHEMA_ERROR_NOMEM) { + /* The registry did not ref sch (NOMEM precedes the ref); unref it and + * return NULL instead of a dangling pointer */ + celix_jansson_schema_unref(sch); + return NULL; + } + /* DUPLICATE_URI is benign: the fragment may already be registered */ + celix_jansson_schema_unref(sch); /* registry holds the owning ref now */ + + return sch; +} + +/* ── Helper: count total unresolved refs across all files ───────────────── */ +static size_t total_unresolved(const celix_jansson_schema_root_t* root) { + size_t count = 0; + CELIX_STRING_HASH_MAP_ITERATE(root->files, iter) { + celix_jansson_schema_file_t* sf = (celix_jansson_schema_file_t*)iter.value.ptrValue; + count += celix_stringHashMap_size(sf->unresolved); + } + return count; +} + +/* ── Phase 2: compile external document ─────────────────────────────────── */ +static int compile_external_document(celix_jansson_schema_root_t* root, const char* location) { + if (!root->loader) + return CELIX_JANSSON_SCHEMA_ERROR_LOADER; + + json_t* doc = NULL; + int rc = root->loader(location, &doc, root->loader_ud); + if (rc != CELIX_JANSSON_SCHEMA_OK || !doc) { + json_decref(doc); + return rc ? rc : CELIX_JANSSON_SCHEMA_ERROR_LOADER; + } + + celix_auto(celix_jansson_uri_t) retr = {0}; + if (celix_jansson_uri_init(&retr, location) != 0) { + json_decref(doc); /* doc has not been transferred to sf->document */ + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* init leaves retr cleared */ + } + celix_jansson_schema_node_t* sch = NULL; + rc = schema_make_internal_depth(doc, root, &retr, NULL, &sch, 0); + if (rc != CELIX_JANSSON_SCHEMA_OK || !sch) { + json_decref(doc); + return rc ? rc : CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + + /* Register root at retrieval URI — auto-resolves waiting fragment="" placeholder */ + int ir = celix_jansson_schema_root_insert(root, &retr, sch); + if (ir == CELIX_JANSSON_SCHEMA_ERROR_NOMEM) { + /* The registry did not ref sch; propagate the OOM instead of returning a + * fake OK that would leave the placeholder unresolved forever */ + json_decref(doc); /* doc has not been transferred to sf->document */ + celix_jansson_schema_unref(sch); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + /* DUPLICATE_URI is benign: a document whose own $id equals the retrieval URI + * was already registered during make */ + + /* Retain document for fragment resolution */ + celix_jansson_schema_file_t* sf = celix_jansson_schema_root_get_or_create_file(root, location); + if (sf) { + json_decref(sf->document); + sf->document = doc; /* ownership transferred */ + /* strdup-then-swap: on OOM keep the old (content-identical) base_uri */ + char* nb = strdup(location); + if (nb) { + free(sf->base_uri); + sf->base_uri = nb; + } + //LCOV_EXCL_START: unreachable — callers guarantee the file already exists + } else { + json_decref(doc); + } + //LCOV_EXCL_STOP + + celix_jansson_schema_unref(sch); + return CELIX_JANSSON_SCHEMA_OK; +} + +/* ── Phase 2: try to resolve a single placeholder ──────────────────────── */ +static bool resolve_placeholder(celix_jansson_schema_root_t* root, + const char* location, const char* fragment, + celix_jansson_schema_node_t* ref_node) { + /* Use the location+fragment to look up in schemas */ + celix_jansson_schema_file_t* sf = + (celix_jansson_schema_file_t*)celix_stringHashMap_get(root->files, location); + assert(sf != NULL); /* sole caller resolve_external_refs Phase B already guarantees sf */ + + /* Already resolved? */ + celix_jansson_schema_node_t* existing = + (celix_jansson_schema_node_t*)celix_stringHashMap_get(sf->schemas, fragment); + if (existing) { + /* ref_node is the REF-kind placeholder the caller fetched from sf->unresolved + * (same table and key this function was called with), so wire it directly */ + ref_node->u.ref.target_weak = existing; + /* the map's removed callback unrefs the value, so ref first: the ref + * that survives the remove is handed over to sf->retained below */ + celix_jansson_schema_ref(ref_node); + if (celix_jansson_vec_push(&sf->retained, ref_node) != 0) { + /* Drop only the extra ref: the placeholder must stay alive in the + * unresolved map because ref proxies borrow it via target_weak */ + celix_jansson_schema_unref(ref_node); + return false; + } + celix_stringHashMap_remove(sf->unresolved, fragment); + return true; + } + + /* Try document fragment walk */ + if (sf->document || (location[0] == '\0' && root->original_schema)) { + celix_jansson_schema_node_t* resolved = resolve_document_fragment(root, location, fragment, 0); + if (resolved) + return true; + } + + /* Try loading the document if not yet loaded */ + if (sf->document == NULL && location[0] != '\0' && root->loader) { + int rc = compile_external_document(root, location); + if (rc == CELIX_JANSSON_SCHEMA_OK) { + /* Re-check schemas after loading */ + celix_jansson_schema_node_t* reloaded = + (celix_jansson_schema_node_t*)celix_stringHashMap_get(sf->schemas, fragment); + if (reloaded) + return true; + /* Try fragment walk again */ + return resolve_document_fragment(root, location, fragment, 0) != NULL; + } + } + + return false; +} + +/* ── Phase 2: resolve all external refs ────────────────────────────────── */ +static int resolve_external_refs(celix_jansson_schema_root_t* root) { + int max_iter = 20; + while (total_unresolved(root) > 0 && max_iter-- > 0) { + bool progress = false; + + /* Phase A: load documents for locations with pending refs */ + { + /* Snapshot file keys — loading adds new entries */ + celix_jansson_vec_t locs; + celix_jansson_vec_init(&locs); + CELIX_STRING_HASH_MAP_ITERATE(root->files, iter) { + char* lk = strdup(iter.key); + if (!lk || celix_jansson_vec_push(&locs, lk) != 0) { + free(lk); /* vec_push failure leaves the vec unchanged */ + for (size_t j = 0; j < locs.len; j++) + free(locs.items[j]); + celix_jansson_vec_free(&locs); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + } + + for (size_t i = 0; i < locs.len; i++) { + const char* loc = (const char*)locs.items[i]; + if (loc[0] == '\0') { free(locs.items[i]); continue; } + + celix_jansson_schema_file_t* sf = + (celix_jansson_schema_file_t*)celix_stringHashMap_get(root->files, loc); + if (sf && celix_stringHashMap_size(sf->unresolved) > 0 && sf->document == NULL) { + int rc = compile_external_document(root, loc); + if (rc != CELIX_JANSSON_SCHEMA_OK) { + for (size_t j = i; j < locs.len; j++) + free(locs.items[j]); + celix_jansson_vec_free(&locs); + return rc; + } + progress = true; + } + free(locs.items[i]); + } + celix_jansson_vec_free(&locs); + } + + /* Phase B: resolve placeholders */ + { + /* Snapshot (location, fragment) pairs */ + struct pf_pair { char* loc; char* frag; }; + celix_jansson_vec_t pairs; + celix_jansson_vec_init(&pairs); + CELIX_STRING_HASH_MAP_ITERATE(root->files, fiter) { + celix_jansson_schema_file_t* sf = (celix_jansson_schema_file_t*)fiter.value.ptrValue; + if (!sf || celix_stringHashMap_size(sf->unresolved) == 0) + continue; + CELIX_STRING_HASH_MAP_ITERATE(sf->unresolved, uiter) { + struct pf_pair* p = (struct pf_pair*)malloc(sizeof(*p)); + if (!p) + goto pairs_cleanup; + p->loc = strdup(fiter.key); + p->frag = strdup(uiter.key); + if (!p->loc || !p->frag || celix_jansson_vec_push(&pairs, p) != 0) { + free(p->loc); + free(p->frag); + free(p); + goto pairs_cleanup; + } + } + } + + for (size_t i = 0; i < pairs.len; i++) { + struct pf_pair* p = (struct pf_pair*)pairs.items[i]; + celix_jansson_schema_file_t* sf = + (celix_jansson_schema_file_t*)celix_stringHashMap_get(root->files, p->loc); + if (sf) { + celix_jansson_schema_node_t* ref_node = + (celix_jansson_schema_node_t*)celix_stringHashMap_get(sf->unresolved, p->frag); + if (ref_node) + progress |= resolve_placeholder(root, p->loc, p->frag, ref_node); + } + } + pairs_cleanup: + for (size_t i = 0; i < pairs.len; i++) { + struct pf_pair* p = (struct pf_pair*)pairs.items[i]; + free(p->loc); + free(p->frag); + free(p); + } + celix_jansson_vec_free(&pairs); + } + + if (!progress) + break; + } + + return total_unresolved(root) > 0 + ? (root->loader ? CELIX_JANSSON_SCHEMA_ERROR_REF_UNRESOLVED + : CELIX_JANSSON_SCHEMA_ERROR_LOADER) + : CELIX_JANSSON_SCHEMA_OK; +} + +/* ════════════════════════════════════════════════════════════════════════ + * Registry + * ════════════════════════════════════════════════════════════════════════ */ + +static void celix_jansson_schema_file_free(void* value); + +celix_jansson_schema_file_t* celix_jansson_schema_root_get_or_create_file(celix_jansson_schema_root_t* root, + const char* location) { + celix_jansson_schema_file_t* sf = + (celix_jansson_schema_file_t*)celix_stringHashMap_get(root->files, location); + if (sf) + return sf; + sf = (celix_jansson_schema_file_t*)calloc(1, sizeof(*sf)); + if (!sf) + return NULL; + sf->schemas = obj_node_map_create(); + sf->unresolved = obj_node_map_create(); + if (!sf->schemas || !sf->unresolved) { + celix_stringHashMap_destroy(sf->schemas); + celix_stringHashMap_destroy(sf->unresolved); + free(sf); + return NULL; + } + celix_jansson_vec_init(&sf->retained); + if (celix_stringHashMap_put(root->files, location, sf) != 0) { + /* The map did not take ownership; release the file directly so no + * half-registered entry is handed to the caller */ + celix_jansson_schema_file_free(sf); + return NULL; + } + return sf; +} + +int celix_jansson_schema_root_insert(celix_jansson_schema_root_t* root, + const celix_jansson_uri_t* uri, + celix_jansson_schema_node_t* sch) { + char* loc = celix_jansson_uri_location(uri); + const char* frag = celix_jansson_uri_fragment(uri); + if (!loc || !frag) { + free(loc); + free((char*)frag); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + celix_jansson_schema_file_t* sf = celix_jansson_schema_root_get_or_create_file(root, loc); + if (!sf) { + free(loc); + free((char*)frag); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + if (celix_stringHashMap_get(sf->schemas, frag)) { + free(loc); + free((char*)frag); + return CELIX_JANSSON_SCHEMA_ERROR_DUPLICATE_URI; + } + celix_jansson_schema_ref(sch); + if (celix_stringHashMap_put(sf->schemas, frag, sch) != 0) { + /* Undo the ref: the map did not take it. Failing here (instead of + * reporting OK) prevents a silent half-registration that would later + * surface as a confusing REF_UNRESOLVED */ + celix_jansson_schema_unref(sch); + free(loc); + free((char*)frag); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + + /* Resolve waiting refs */ + celix_jansson_schema_node_t* waiting = + (celix_jansson_schema_node_t*)celix_stringHashMap_get(sf->unresolved, frag); + if (waiting && waiting->kind == CELIX_JANSSON_SCHEMA_KIND_REF) { + waiting->u.ref.target_weak = sch; + /* the map's removed callback unrefs the value, so ref first: the ref + * that survives the remove is handed over to sf->retained below */ + celix_jansson_schema_ref(waiting); + if (celix_jansson_vec_push(&sf->retained, waiting) != 0) { + /* Drop only the extra ref: the placeholder must stay alive in the + * unresolved map because ref proxies borrow it via target_weak */ + celix_jansson_schema_unref(waiting); + free(loc); + free((char*)frag); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + celix_stringHashMap_remove(sf->unresolved, frag); + } + free(loc); + free((char*)frag); + return CELIX_JANSSON_SCHEMA_OK; +} + +static void celix_jansson_schema_file_free(void* value) { + celix_jansson_schema_file_t* sf = (celix_jansson_schema_file_t*)value; + assert(sf != NULL); + celix_stringHashMap_destroy(sf->schemas); /* values freed via creation-time removed callback */ + celix_stringHashMap_destroy(sf->unresolved); + for (size_t i = 0; i < sf->retained.len; i++) + celix_jansson_schema_unref((celix_jansson_schema_node_t*)sf->retained.items[i]); + celix_jansson_vec_free(&sf->retained); + json_decref(sf->document); + free(sf->base_uri); + free(sf); +} + +void celix_jansson_schema_root_destroy(celix_jansson_schema_root_t* root) { + if (!root) + return; + celix_jansson_schema_unref(root->root); + json_decref(root->original_schema); + celix_stringHashMap_destroy(root->files); /* values freed via creation-time removed callback */ + free(root); +} + +int celix_jansson_schema_root_validate(celix_jansson_schema_root_t* root, + const char* initial_uri, + json_t* instance, + celix_jansson_validation_context_t* ctx) { + if (!root || !ctx) + return -1; + + celix_jansson_schema_node_t* sch = NULL; + + /* Resolve initial_uri to a specific subschema, if provided */ + if (initial_uri && initial_uri[0] != '\0' && strcmp(initial_uri, "#") != 0) { + celix_auto(celix_jansson_uri_t) uri = {0}; + if (celix_jansson_uri_init(&uri, initial_uri) == 0) { + char* loc = celix_jansson_uri_location(&uri); + const char* frag = celix_jansson_uri_fragment(&uri); + + if (loc) { + celix_jansson_schema_file_t* sf = + (celix_jansson_schema_file_t*)celix_stringHashMap_get(root->files, loc); + if (sf) { + if (frag && frag[0] != '\0') { + sch = (celix_jansson_schema_node_t*)celix_stringHashMap_get(sf->schemas, frag); + if (!sch) { + /* Attempt on-demand compilation from the original document */ + sch = resolve_document_fragment(root, loc, frag, 0); + } + } else { + /* Empty fragment — resolve the root of this file */ + sch = (celix_jansson_schema_node_t*)celix_stringHashMap_get(sf->schemas, ""); + } + } + } + free(loc); + free((char*)frag); + } + } + + /* Fallback: use root schema */ + if (!sch) + sch = root->root; + + if (!sch) { + ctx->sink->emit(ctx->sink, "", instance, "no root schema set"); + return 1; + } + + celix_auto(celix_jansson_path_t) path = {0}; + int errs = sch->vtable->validate(sch, instance, &path, ctx); + return errs; +} + +/* ════════════════════════════════════════════════════════════════════════ + * Error list + * ════════════════════════════════════════════════════════════════════════ */ + +void celix_jansson_error_list_init(celix_jansson_error_list_t* el) { memset(el, 0, sizeof(*el)); } +void celix_jansson_error_list_add(celix_jansson_error_list_t* el, const char* ptr, json_t* inst, const char* msg) { + if (el->len >= el->cap) { + size_t nc = el->cap ? el->cap * 2 : 8; + celix_jansson_error_entry_t* ne = (celix_jansson_error_entry_t*)realloc(el->entries, nc * sizeof(*ne)); + if (!ne) + return; + el->entries = ne; + el->cap = nc; + } + celix_jansson_error_entry_t* e = &el->entries[el->len++]; + e->ptr = strdup(ptr); + e->message = strdup(msg); + e->instance = inst; + json_incref(inst); +} +void celix_jansson_error_list_clear(celix_jansson_error_list_t* el) { + for (size_t i = 0; i < el->len; i++) { + free(el->entries[i].ptr); + free(el->entries[i].message); + json_decref(el->entries[i].instance); + } + free(el->entries); + memset(el, 0, sizeof(*el)); +} + +/* ════════════════════════════════════════════════════════════════════════ + * Public API + * ════════════════════════════════════════════════════════════════════════ */ + +struct celix_jansson_schema_validator_t { + celix_jansson_schema_root_t* root; + bool abort_on_error; +}; + +celix_jansson_schema_validator_t* celix_jansson_schema_validator_create(celix_jansson_schema_loader_fn loader, + void* loader_ud, + celix_jansson_schema_format_checker_fn format, + void* format_ud, + celix_jansson_schema_content_checker_fn content, + void* content_ud) { + celix_jansson_schema_validator_t* v = (celix_jansson_schema_validator_t*)calloc(1, sizeof(*v)); + if (!v) + return NULL; + v->root = (celix_jansson_schema_root_t*)calloc(1, sizeof(*v->root)); + if (!v->root) { + free(v); + return NULL; + } + celix_string_hash_map_create_options_t opts = CELIX_EMPTY_STRING_HASH_MAP_CREATE_OPTIONS; + opts.simpleRemovedCallback = celix_jansson_schema_file_free; + v->root->files = celix_stringHashMap_createWithOptions(&opts); /* values freed via removed callback */ + if (!v->root->files) { + free(v->root); + free(v); + return NULL; + } + v->root->loader = loader; + v->root->loader_ud = loader_ud; + v->root->format = format; + v->root->format_ud = format_ud; + v->root->content = content; + v->root->content_ud = content_ud; + return v; +} + +void celix_jansson_schema_validator_destroy(celix_jansson_schema_validator_t* v) { + if (!v) + return; + celix_jansson_schema_root_destroy(v->root); + free(v); +} + +void celix_jansson_schema_validator_set_abort_on_error(celix_jansson_schema_validator_t* v, bool enable) { + if (v) + v->abort_on_error = enable; +} + +int celix_jansson_schema_set_root_schema(celix_jansson_schema_validator_t* v, json_t* schema, char** errmsg) { + if (!v || !schema) { + if (errmsg) + *errmsg = strdup("validator and schema required"); + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + } + + celix_jansson_schema_root_t* root = v->root; + celix_jansson_schema_unref(root->root); + root->root = NULL; + json_decref(root->original_schema); + root->original_schema = NULL; + celix_stringHashMap_clear(root->files); /* values freed via creation-time removed callback */ + + json_t* copy = json_deep_copy(schema); + if (!copy) { + if (errmsg) + *errmsg = strdup(celix_jansson_schema_strerror(CELIX_JANSSON_SCHEMA_ERROR_NOMEM)); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + + root->original_schema = json_deep_copy(schema); + if (!root->original_schema) { + /* The first copy (L2446) is still owned by this frame; the decref at + * the end of the compile path (after schema_make_internal_depth) is not + * reached here, so release it explicitly. */ + json_decref(copy); + if (errmsg) + *errmsg = strdup(celix_jansson_schema_strerror(CELIX_JANSSON_SCHEMA_ERROR_NOMEM)); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + + celix_auto(celix_jansson_uri_t) root_base = {0}; + + celix_jansson_schema_node_t* sch = NULL; + int err = schema_make_internal_depth(copy, root, NULL, &root_base, &sch, 0); + json_decref(copy); + if (err != CELIX_JANSSON_SCHEMA_OK) { + if (errmsg) + *errmsg = strdup(celix_jansson_schema_strerror(err)); + return err; + } + + root->root = sch; + int rir = celix_jansson_schema_root_insert(root, &root_base, sch); + if (rir == CELIX_JANSSON_SCHEMA_ERROR_NOMEM) { + /* root_insert's NOMEM happens before its registry ref (L2256), so the + * registry holds no reference; root->root still owns the only one. + * Unref it and leave the root in the same clean state as the other + * failing paths below (root->root == NULL). */ + celix_jansson_schema_unref(root->root); + root->root = NULL; + if (errmsg) + *errmsg = strdup(celix_jansson_schema_strerror(CELIX_JANSSON_SCHEMA_ERROR_NOMEM)); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + /* DUPLICATE_URI is benign here: root->files was cleared above (L2444), so the + * only way the "" key already exists is the top-level $id:"" registration done + * during make (L1853); the registry already holds a reference in that case. */ + + { + char* rloc = celix_jansson_uri_location(&root_base); + if (rloc) { + celix_jansson_schema_file_t* rsf = celix_jansson_schema_root_get_or_create_file(root, rloc); + if (rsf) { + json_decref(rsf->document); + rsf->document = json_incref(root->original_schema); + /* strdup-then-swap: on OOM keep the old (content-identical) base_uri */ + char* nb = strdup(rloc); + if (nb) { + free(rsf->base_uri); + rsf->base_uri = nb; + } + } + } + free(rloc); + } + + err = resolve_external_refs(root); + if (err != CELIX_JANSSON_SCHEMA_OK && errmsg) + *errmsg = strdup(celix_jansson_schema_strerror(err)); + return err; +} + +int celix_jansson_schema_validate(celix_jansson_schema_validator_t* v, + json_t* instance, + celix_jansson_schema_error_fn on_error, + void* error_ud, + json_t** patch_out) { + if (!v) + return -1; + user_sink_t* sink = (user_sink_t*)calloc(1, sizeof(*sink)); + if (!sink) + return -1; + sink->base.emit = user_emit; + sink->base.destroy = user_destroy; + sink->fn = on_error; + sink->ud = error_ud; + + celix_jansson_validation_context_t ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.root = v->root; + ctx.sink = &sink->base; + ctx.patch = json_array(); + if (!ctx.patch) { + /* OOM: fail the validation instead of running with a broken patch + * (default-value fill-ins would be silently dropped) */ + sink->base.destroy(&sink->base); + return -1; + } + + sink->abort_on_error = v->abort_on_error; + sink->ctx = &ctx; + + int errs = celix_jansson_schema_root_validate(v->root, "#", instance, &ctx); + + if (patch_out) + *patch_out = ctx.patch; + else + json_decref(ctx.patch); + + sink->base.destroy(&sink->base); + return errs; +} + +int celix_jansson_schema_validate_uri(celix_jansson_schema_validator_t* v, + json_t* instance, + const char* initial_uri, + celix_jansson_schema_error_fn on_error, + void* error_ud, + json_t** patch_out) { + if (!v) + return -1; + user_sink_t* sink = (user_sink_t*)calloc(1, sizeof(*sink)); + if (!sink) + return -1; + sink->base.emit = user_emit; + sink->base.destroy = user_destroy; + sink->fn = on_error; + sink->ud = error_ud; + + celix_jansson_validation_context_t ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.root = v->root; + ctx.sink = &sink->base; + ctx.patch = json_array(); + if (!ctx.patch) { + /* OOM: fail the validation instead of running with a broken patch + * (default-value fill-ins would be silently dropped) */ + sink->base.destroy(&sink->base); + return -1; + } + + sink->abort_on_error = v->abort_on_error; + sink->ctx = &ctx; + + int errs = celix_jansson_schema_root_validate(v->root, initial_uri ? initial_uri : "#", instance, &ctx); + + if (patch_out) + *patch_out = ctx.patch; + else + json_decref(ctx.patch); + + sink->base.destroy(&sink->base); + return errs; +} + +json_t* celix_jansson_schema_draft7_meta_schema(void) { + /* Full draft-07 meta-schema (from json-schema-org/JSON-Schema-Test-Suite, + * as embedded in the reference json-schema-validator project). + * A stub (type-only) here lets instances like {"minLength": -1} pass the + * "remote ref, containing refs itself" suite cases that must be rejected. */ + const char* s = "{\"$schema\":\"http://json-schema.org/draft-07/schema#\"," + "\"$id\":\"http://json-schema.org/draft-07/schema#\",\"title\":\"Core schema meta-schema\"," + "\"definitions\":{\"schemaArray\":{\"type\":\"array\",\"minItems\":1,\"items\":{\"$ref\":\"#\"}}," + "\"nonNegativeInteger\":{\"type\":\"integer\",\"minimum\":0}," + "\"nonNegativeIntegerDefault0\":{\"allOf\":[{\"$ref\":\"#/definitions/nonNegativeInteger\"}," + "{\"default\":0}]},\"simpleTypes\":{\"enum\":[\"array\",\"boolean\",\"integer\",\"null\",\"number\"," + "\"object\",\"string\"]},\"stringArray\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}," + "\"uniqueItems\":true,\"default\":[]}},\"type\":[\"object\",\"boolean\"]," + "\"properties\":{\"$id\":{\"type\":\"string\",\"format\":\"uri-reference\"}," + "\"$schema\":{\"type\":\"string\",\"format\":\"uri\"},\"$ref\":{\"type\":\"string\"," + "\"format\":\"uri-reference\"},\"$comment\":{\"type\":\"string\"},\"title\":{\"type\":\"string\"}," + "\"description\":{\"type\":\"string\"},\"default\":true,\"readOnly\":{\"type\":\"boolean\"," + "\"default\":false},\"examples\":{\"type\":\"array\",\"items\":true}," + "\"multipleOf\":{\"type\":\"number\",\"exclusiveMinimum\":0},\"maximum\":{\"type\":\"number\"}," + "\"exclusiveMaximum\":{\"type\":\"number\"},\"minimum\":{\"type\":\"number\"}," + "\"exclusiveMinimum\":{\"type\":\"number\"}," + "\"maxLength\":{\"$ref\":\"#/definitions/nonNegativeInteger\"}," + "\"minLength\":{\"$ref\":\"#/definitions/nonNegativeIntegerDefault0\"}," + "\"pattern\":{\"type\":\"string\",\"format\":\"regex\"},\"additionalItems\":{\"$ref\":\"#\"}," + "\"items\":{\"anyOf\":[{\"$ref\":\"#\"},{\"$ref\":\"#/definitions/schemaArray\"}],\"default\":true}," + "\"maxItems\":{\"$ref\":\"#/definitions/nonNegativeInteger\"}," + "\"minItems\":{\"$ref\":\"#/definitions/nonNegativeIntegerDefault0\"}," + "\"uniqueItems\":{\"type\":\"boolean\",\"default\":false},\"contains\":{\"$ref\":\"#\"}," + "\"maxProperties\":{\"$ref\":\"#/definitions/nonNegativeInteger\"}," + "\"minProperties\":{\"$ref\":\"#/definitions/nonNegativeIntegerDefault0\"}," + "\"required\":{\"$ref\":\"#/definitions/stringArray\"},\"additionalProperties\":{\"$ref\":\"#\"}," + "\"definitions\":{\"type\":\"object\",\"additionalProperties\":{\"$ref\":\"#\"},\"default\":{}}," + "\"properties\":{\"type\":\"object\",\"additionalProperties\":{\"$ref\":\"#\"},\"default\":{}}," + "\"patternProperties\":{\"type\":\"object\",\"additionalProperties\":{\"$ref\":\"#\"}," + "\"propertyNames\":{\"format\":\"regex\"},\"default\":{}},\"dependencies\":{\"type\":\"object\"," + "\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#\"},{\"$ref\":\"#/definitions/stringArray\"}]}}," + "\"propertyNames\":{\"$ref\":\"#\"},\"const\":true,\"enum\":{\"type\":\"array\",\"items\":true," + "\"minItems\":1,\"uniqueItems\":true},\"type\":{\"anyOf\":[{\"$ref\":\"#/definitions/simpleTypes\"}," + "{\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/simpleTypes\"},\"minItems\":1," + "\"uniqueItems\":true}]},\"format\":{\"type\":\"string\"},\"contentMediaType\":{\"type\":\"string\"}," + "\"contentEncoding\":{\"type\":\"string\"},\"if\":{\"$ref\":\"#\"},\"then\":{\"$ref\":\"#\"}," + "\"else\":{\"$ref\":\"#\"},\"allOf\":{\"$ref\":\"#/definitions/schemaArray\"}," + "\"anyOf\":{\"$ref\":\"#/definitions/schemaArray\"}," + "\"oneOf\":{\"$ref\":\"#/definitions/schemaArray\"},\"not\":{\"$ref\":\"#\"}},\"default\":true}"; + return json_loads(s, 0, NULL); +} diff --git a/libs/jansson_ext/src/celix_jansson_uri.c b/libs/jansson_ext/src/celix_jansson_uri.c new file mode 100644 index 000000000..3393a8b61 --- /dev/null +++ b/libs/jansson_ext/src/celix_jansson_uri.c @@ -0,0 +1,424 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "celix_jansson_uri.h" +#include "celix_stdlib_cleanup.h" +#include "celix_util.h" +#include +#include +#include +#include +#include + +/* ── URI ──────────────────────────────────────────────────────────────── */ + +/* Percent-decode a string. Returns malloc'd output. */ +static char* percent_decode(const char* src) { + celix_auto(celix_jansson_strbuf_t) sb; + celix_jansson_strbuf_init(&sb); + + for (const char* s = src; *s; s++) { + int rc; + if (*s == '%' && isxdigit((unsigned char)s[1]) && isxdigit((unsigned char)s[2])) { + unsigned int val; + char hex[3] = {s[1], s[2], '\0'}; + sscanf(hex, "%2x", &val); + rc = celix_jansson_strbuf_appendc(&sb, (char)val); + s += 2; + } else { + rc = celix_jansson_strbuf_appendc(&sb, *s); + } + if (rc != 0) { + /* OOM: sb is released automatically; update() maps NULL to NOMEM */ + return NULL; + } + } + + return celix_jansson_strbuf_detach(&sb); +} + +int celix_jansson_uri_init(celix_jansson_uri_t* u, const char* uri_str) { + memset(u, 0, sizeof(*u)); + int rc = celix_jansson_uri_update(u, uri_str); + if (rc != 0) + celix_jansson_uri_clear(u); /* leave u fully cleared on failure */ + return rc; +} + +int celix_jansson_uri_update(celix_jansson_uri_t* u, const char* uri_str) { + if (!uri_str) + return 0; /* NULL is a no-op (keep current contents); "" clears instead */ + + /* Split at first '#' */ + const char* hash = strchr(uri_str, '#'); + size_t loc_len = hash ? (size_t)(hash - uri_str) : strlen(uri_str); + bool fragment_only = (loc_len == 0 && hash != NULL); + bool has_scheme = (loc_len > 0 && strstr(uri_str, "://") != NULL); + + /* Save old components for relative resolution. Only needed for relative + * refs (no scheme, non-fragment-only); each strdup is checked individually + * so no OOM goes unhandled — u is not yet modified here. The buffers are + * freed automatically on every exit path (celix_autofree). */ + celix_autofree char* old_path = NULL; + celix_autofree char* old_scheme = NULL; + celix_autofree char* old_authority = NULL; + if (loc_len > 0 && !has_scheme) { + old_path = u->path ? strdup(u->path) : NULL; + if (u->path && !old_path) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* autofree releases old-* */ + old_scheme = u->scheme ? strdup(u->scheme) : NULL; + if (u->scheme && !old_scheme) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* autofree releases old-* */ + old_authority = u->authority ? strdup(u->authority) : NULL; + if (u->authority && !old_authority) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* autofree releases old-* */ + } + + /* Fragment-only refs keep the location, only update fragment */ + if (fragment_only) { + /* Only clear the fragment part */ + free(u->identifier); + u->identifier = NULL; + celix_json_pointer_clear(&u->pointer); + } else { + /* Free ALL existing components */ + celix_jansson_uri_clear(u); + /* Restore scheme/authority for relative paths that don't replace them */ + if (old_scheme) { + u->scheme = celix_steal_ptr(old_scheme); + } + if (old_authority) { + u->authority = celix_steal_ptr(old_authority); + } + } + + /* Decode the location part (before '#'); freed automatically on every + * exit path (celix_autofree) unless ownership is transferred to u->urn */ + celix_autofree char* location = NULL; + if (loc_len > 0) { + location = (char*)malloc(loc_len + 1); + if (!location) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + memcpy(location, uri_str, loc_len); + location[loc_len] = '\0'; + } + + /* Decode the fragment (after '#') */ + if (hash && hash[1]) { + const char* frag = hash + 1; + + /* Percent-decode the fragment */ + celix_autofree char* decoded = percent_decode(frag); + if (!decoded) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* autofree releases location/old-* */ + + /* Fragment classification */ + if (decoded[0] == '/') { + /* JSON Pointer */ + if (celix_json_pointer_init(&u->pointer, decoded) != 0) { + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* autofree releases location/old-* */ + } + } else { + /* Plain-name identifier */ + u->identifier = celix_steal_ptr(decoded); + } + } + + /* Parse location */ + if (location) { + /* Check for URN */ + if (strncmp(location, "urn:", 4) == 0) { + u->urn = celix_steal_ptr(location); + } else { + /* URL parsing */ + const char* p = location; + /* Scheme */ + const char* colon = strstr(p, "://"); + if (colon) { + size_t scheme_len = (size_t)(colon - p); + u->scheme = (char*)malloc(scheme_len + 1); + if (!u->scheme) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* autofree releases location/old-* */ + memcpy(u->scheme, p, scheme_len); + u->scheme[scheme_len] = '\0'; + p = colon + 3; + /* Authority */ + const char* slash = strchr(p, '/'); + if (slash) { + size_t auth_len = (size_t)(slash - p); + u->authority = (char*)malloc(auth_len + 1); + if (!u->authority) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* autofree releases location/old-* */ + memcpy(u->authority, p, auth_len); + u->authority[auth_len] = '\0'; + u->path = strdup(slash); + if (!u->path) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* autofree releases location/old-* */ + } else { + u->authority = strdup(p); + if (!u->authority) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* autofree releases location/old-* */ + u->path = NULL; + } + } else { + /* Relative path (no scheme://) */ + if (p[0] == '/') { + /* Absolute path */ + u->path = strdup(p); + if (!u->path) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* autofree releases location/old-* */ + } else if (old_path && *p) { + /* Resolve relative to old path's directory */ + const char* last_slash = strrchr(old_path, '/'); + if (last_slash) { + size_t dir_len = (size_t)(last_slash - old_path); + celix_auto(celix_jansson_strbuf_t) sb; + celix_jansson_strbuf_init(&sb); + if (celix_jansson_strbuf_append(&sb, old_path, dir_len) != 0 || + celix_jansson_strbuf_appendc(&sb, '/') != 0 || + celix_jansson_strbuf_appends(&sb, p) != 0) { + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* sb released automatically */ + } + u->path = celix_jansson_strbuf_detach(&sb); + } else { + u->path = strdup(p); + if (!u->path) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* autofree releases location/old-* */ + } + } else { + u->path = strdup(p); + if (!u->path) + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; /* autofree releases location/old-* */ + } + } + } + } + + return 0; +} + +int celix_jansson_uri_derive(const celix_jansson_uri_t* base, const char* uri_str, celix_jansson_uri_t* out) { + /* Reset out first (defensive: safe if caller reuses the buffer); + * base is copied by the strdup block below */ + celix_jansson_uri_clear(out); + + /* Copy base components; each strdup is checked individually so no OOM + * goes unhandled (on failure out is left fully cleared) */ + out->scheme = base->scheme ? strdup(base->scheme) : NULL; + if (base->scheme && !out->scheme) { + celix_jansson_uri_clear(out); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + out->authority = base->authority ? strdup(base->authority) : NULL; + if (base->authority && !out->authority) { + celix_jansson_uri_clear(out); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + out->path = base->path ? strdup(base->path) : NULL; + if (base->path && !out->path) { + celix_jansson_uri_clear(out); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + out->urn = base->urn ? strdup(base->urn) : NULL; + if (base->urn && !out->urn) { + celix_jansson_uri_clear(out); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + if (base->identifier) { + out->identifier = strdup(base->identifier); + if (!out->identifier) { + celix_jansson_uri_clear(out); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + } else { + /* Copy pointer tokens */ + for (size_t i = 0; i < base->pointer.len; i++) { + if (celix_json_pointer_push(&out->pointer, base->pointer.tokens[i]) != 0) { + celix_jansson_uri_clear(out); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + } + } + + /* Now resolve uri_str relative to this */ + int rc = celix_jansson_uri_update(out, uri_str); + if (rc != 0) + celix_jansson_uri_clear(out); /* release the copied base components on failure */ + return rc; +} + +int celix_jansson_uri_append(const celix_jansson_uri_t* u, const char* token, celix_jansson_uri_t* out) { + /* Reset out first (defensive: safe if caller reuses the buffer); + * u is copied by the strdup block below; each strdup is checked individually + * so no OOM goes unhandled (on failure out is left fully cleared) */ + celix_jansson_uri_clear(out); + out->scheme = u->scheme ? strdup(u->scheme) : NULL; + if (u->scheme && !out->scheme) { + celix_jansson_uri_clear(out); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + out->authority = u->authority ? strdup(u->authority) : NULL; + if (u->authority && !out->authority) { + celix_jansson_uri_clear(out); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + out->path = u->path ? strdup(u->path) : NULL; + if (u->path && !out->path) { + celix_jansson_uri_clear(out); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + out->urn = u->urn ? strdup(u->urn) : NULL; + if (u->urn && !out->urn) { + celix_jansson_uri_clear(out); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + + if (u->identifier) { + out->identifier = strdup(u->identifier); + if (!out->identifier) { + celix_jansson_uri_clear(out); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + return 0; /* no-op for identifier URIs */ + } + + /* Copy existing pointer tokens */ + for (size_t i = 0; i < u->pointer.len; i++) { + if (celix_json_pointer_push(&out->pointer, u->pointer.tokens[i]) != 0) { + celix_jansson_uri_clear(out); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + } + /* Append new token */ + if (celix_json_pointer_push(&out->pointer, token) != 0) { + celix_jansson_uri_clear(out); + return CELIX_JANSSON_SCHEMA_ERROR_NOMEM; + } + + return 0; +} + +char* celix_jansson_uri_location(const celix_jansson_uri_t* u) { + if (u->urn) { + return strdup(u->urn); /* NULL only on allocation failure (OOM signal) */ + } + + celix_auto(celix_jansson_strbuf_t) sb; + celix_jansson_strbuf_init(&sb); + if (u->scheme) { + if (celix_jansson_strbuf_appends(&sb, u->scheme) != 0 || + celix_jansson_strbuf_appends(&sb, "://") != 0) + return NULL; /* OOM signal; sb released automatically */ + } + if (u->authority && celix_jansson_strbuf_appends(&sb, u->authority) != 0) + return NULL; /* OOM signal */ + if (u->path && celix_jansson_strbuf_appends(&sb, u->path) != 0) + return NULL; /* OOM signal */ + + if (sb.len == 0) + return strdup(""); /* NULL only on allocation failure */ + + return celix_jansson_strbuf_detach(&sb); /* never NULL when len > 0 */ +} + +char* celix_jansson_uri_to_string(const celix_jansson_uri_t* u) { + celix_autofree char* loc = celix_jansson_uri_location(u); + celix_autofree char* frag = celix_jansson_uri_fragment(u); + + celix_auto(celix_jansson_strbuf_t) sb; + celix_jansson_strbuf_init(&sb); + /* OOM: any allocation failure below propagates as NULL (never a crash); + * loc/frag/sb are released automatically on every exit path */ + if (!loc || !frag) + return NULL; /* OOM signal */ + if (celix_jansson_strbuf_appends(&sb, loc) != 0) + return NULL; /* OOM signal */ + if (*frag) { + if (celix_jansson_strbuf_appends(&sb, "#") != 0 || + celix_jansson_strbuf_appends(&sb, frag) != 0) + return NULL; /* OOM signal */ + } + + if (sb.len == 0) + return strdup(""); /* NULL only on allocation failure */ + + return celix_jansson_strbuf_detach(&sb); +} + +char* celix_jansson_uri_escape(const char* src) { + celix_auto(celix_jansson_strbuf_t) sb; + celix_jansson_strbuf_init(&sb); + + for (const char* c = src; *c; c++) { + int rc; + if (*c == '~') + rc = celix_jansson_strbuf_appends(&sb, "~0"); + else if (*c == '/') + rc = celix_jansson_strbuf_appends(&sb, "~1"); + else + rc = celix_jansson_strbuf_appendc(&sb, *c); + if (rc != 0) + return NULL; /* OOM signal; sb released automatically */ + } + + if (sb.len == 0) + return strdup(""); /* escape("") must return "" rather than NULL */ + + return celix_jansson_strbuf_detach(&sb); +} + +char* celix_jansson_uri_fragment(const celix_jansson_uri_t* u) { + /* Returns NULL only on allocation failure (OOM signal); there is no + * internal crash path. Callers must check for NULL. */ + if (u->identifier) + return strdup(u->identifier); + + if (u->pointer.len > 0) { + return celix_json_pointer_to_string(&u->pointer); + } + + return strdup(""); +} + +bool celix_jansson_uri_equals(const celix_jansson_uri_t* a, const celix_jansson_uri_t* b) { + celix_autofree char* la = celix_jansson_uri_location(a); + celix_autofree char* lb = celix_jansson_uri_location(b); + if (!la || !lb) { /* OOM: degrade to "not equal" instead of strcmp(NULL) */ + return false; + } + int r = strcmp(la, lb); + if (r != 0) + return false; + celix_autofree char* fa = celix_jansson_uri_fragment(a); + celix_autofree char* fb = celix_jansson_uri_fragment(b); + if (!fa || !fb) { + return false; + } + r = strcmp(fa, fb); + return r == 0; +} + +void celix_jansson_uri_clear(celix_jansson_uri_t* u) { + free(u->urn); + free(u->scheme); + free(u->authority); + free(u->path); + free(u->identifier); + celix_json_pointer_clear(&u->pointer); + memset(u, 0, sizeof(*u)); +} diff --git a/libs/jansson_ext/src/celix_jansson_uri.h b/libs/jansson_ext/src/celix_jansson_uri.h new file mode 100644 index 000000000..e13139db0 --- /dev/null +++ b/libs/jansson_ext/src/celix_jansson_uri.h @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#ifndef CELIX_CELIX_JANSSON_URI_H +#define CELIX_CELIX_JANSSON_URI_H + +#include + +#include "celix_cleanup.h" +#include "celix_jansson_schema.h" +#include "celix_jansson_pointer.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ── URI structure ─────────────────────────────────────────────────────── */ + +typedef struct celix_jansson_uri_t { + char* urn; /* "urn:..." or NULL */ + char* scheme; /* e.g., "http" */ + char* authority; /* e.g., "json-schema.org" */ + char* path; /* e.g., "/draft-07/schema" */ + celix_json_pointer_t pointer; /* fragment, when it starts with '/' */ + char* identifier; /* fragment, when it's a plain-name identifier */ +} celix_jansson_uri_t; + +/** Initialize/parse a URI from a string. Returns JSS error code. + * NULL and "" both produce an empty URI (the struct is zeroed first). + * On failure (NOMEM) @p u is left fully cleared. */ +int celix_jansson_uri_init(celix_jansson_uri_t* u, const char* uri_str); + +/** Update a URI in-place by resolving @p uri_str against it. + * NULL keeps the current contents unchanged; "" clears everything + * (empty URI) — the two are NOT equivalent here. + * On failure (NOMEM) @p u may be left partially modified; callers must call + * celix_jansson_uri_clear() (init and derive already clear on failure). */ +int celix_jansson_uri_update(celix_jansson_uri_t* u, const char* uri_str); + +/** + * Derive a new URI by resolving @p uri_str relative to @p base. + * A NULL @p uri_str copies @p base unchanged; "" resolves to an empty URI. + * @p out must be zero-initialized, or hold an existing URI to be reused + * (it is cleared first); never pass uninitialized stack memory. + */ +int celix_jansson_uri_derive(const celix_jansson_uri_t* base, const char* uri_str, celix_jansson_uri_t* out); + +/** + * Append a JSON Pointer token to the URI (no-op if the URI has an + * identifier fragment). + * @p out must be zero-initialized, or hold an existing URI to be reused + * (it is cleared first); never pass uninitialized stack memory. + */ +int celix_jansson_uri_append(const celix_jansson_uri_t* u, const char* token, celix_jansson_uri_t* out); + +/** Reconstruct the location part (scheme://authority/path or URN). Returns malloc'd string; caller must free(). + * Returns NULL on any allocation failure (OOM signal); callers must check. */ +char* celix_jansson_uri_location(const celix_jansson_uri_t* u); + +/** Full URI string "location # fragment". Returns malloc'd string. + * Returns NULL on any allocation failure (OOM signal); callers must check. */ +char* celix_jansson_uri_to_string(const celix_jansson_uri_t* u); + +/** Escape special chars for JSON Pointer (~ and /). Returns malloc'd. + * Returns NULL on any allocation failure (OOM signal); escape("") returns "" (never NULL unless OOM). */ +char* celix_jansson_uri_escape(const char* src); + +/** Return the fragment as a string (concatenation of pointer or identifier). Returns malloc'd string; caller must free(). */ +char* celix_jansson_uri_fragment(const celix_jansson_uri_t* u); + +/** Compare two URIs for equality. Returns true if equal. */ +bool celix_jansson_uri_equals(const celix_jansson_uri_t* a, const celix_jansson_uri_t* b); + +/** Free all memory held by a URI. */ +void celix_jansson_uri_clear(celix_jansson_uri_t* u); + +/* Enables `celix_auto(celix_jansson_uri_t) u;` for scope-based automatic + * cleanup. Safe on zeroed structs (clear() frees NULLs and re-zeroes). */ +CELIX_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(celix_jansson_uri_t, celix_jansson_uri_clear) + +#ifdef __cplusplus +} +#endif + +#endif /* CELIX_CELIX_JANSSON_URI_H */ diff --git a/libs/jansson_ext/src/celix_json_merge_patch.c b/libs/jansson_ext/src/celix_json_merge_patch.c new file mode 100644 index 000000000..6889453c0 --- /dev/null +++ b/libs/jansson_ext/src/celix_json_merge_patch.c @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "celix_json_merge_patch.h" + +/** + * Recursively merge @p patch into @p target. + * + * Consumes @p target (an owned reference from the caller) and returns a + * new owned reference, or NULL on out-of-memory, in which case @p target + * has been released. @p patch is borrowed and never modified. The + * returned document is always distinct from @p patch. + */ +static json_t* merge_patch_recursive(json_t* target, const json_t* patch) { + if (!json_is_object(patch)) { + /* Whole-document replacement: the patch value becomes the result */ + json_t* replacement = json_deep_copy(patch); + json_decref(target); + return replacement; + } + + if (!json_is_object(target)) { + /* An object patch replaces a non-object target with a fresh object */ + json_decref(target); + target = json_object(); + if (!target) + return NULL; + } + + const char* key; + json_t* value; + json_object_foreach((json_t*)patch, key, value) { + if (json_is_null(value)) { + /* A null value removes the member; an absent member is a no-op + * (json_object_del returns -1 without side effects) */ + json_object_del(target, key); + } else { + /* An absent member is treated as starting from null (RFC 7396). + * json_incref never fails; json_null only fails via error injection */ + json_t* sub = json_object_get(target, key); + json_t* arg = sub ? json_incref(sub) : json_null(); + if (!arg) { + json_decref(target); + return NULL; + } + json_t* merged = merge_patch_recursive(arg, value); + if (!merged) { + json_decref(target); + return NULL; + } + /* merged is consumed by a failed set_new as well (jansson + * decrefs the value on all failure paths), so it must not be + * released here */ + if (json_object_set_new(target, key, merged) != 0) { + json_decref(target); + return NULL; + } + } + } + + return target; +} + +json_t* celix_json_merge_patch(const json_t* target, const json_t* patch) { + if (!target || !patch) + return NULL; + + if (!json_is_object(patch)) + return json_deep_copy(patch); /* avoids copying an irrelevant target */ + + json_t* copy = json_deep_copy(target); + if (!copy) + return NULL; + + return merge_patch_recursive(copy, patch); +} diff --git a/libs/jansson_ext/src/celix_json_patch.c b/libs/jansson_ext/src/celix_json_patch.c new file mode 100644 index 000000000..91c352b53 --- /dev/null +++ b/libs/jansson_ext/src/celix_json_patch.c @@ -0,0 +1,276 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "celix_json_patch.h" +#include "celix_jansson_schema.h" +#include "celix_jansson_pointer.h" +#include "celix_cleanup.h" +#include +#include + +int celix_json_patch_add(json_t* patch, const char* path_str, json_t* value) { + if (!patch || !json_is_array(patch)) + return -1; + + /* val/op are auto-released on every early return */ + json_auto_t* val = value; + json_auto_t* op = json_object(); + if (!op) + return -1; + + json_auto_t* op_name = json_string("add"); + if (!op_name) + return -1; + if (json_object_set_new(op, "op", celix_steal_ptr(op_name)) != 0) + return -1; + + json_auto_t* path = json_string(path_str); + if (!path) + return -1; + if (json_object_set_new(op, "path", celix_steal_ptr(path)) != 0) + return -1; + + if (json_object_set_new(op, "value", celix_steal_ptr(val)) != 0) + return -1; + + /* on failure the op (and with it the value) is consumed */ + return json_array_append_new(patch, celix_steal_ptr(op)); +} + +int celix_json_patch_replace(json_t* patch, const char* path_str, json_t* value) { + if (!patch || !json_is_array(patch)) + return -1; + + /* val/op are auto-released on every early return */ + json_auto_t* val = value; + json_auto_t* op = json_object(); + if (!op) + return -1; + + json_auto_t* op_name = json_string("replace"); + if (!op_name) + return -1; + if (json_object_set_new(op, "op", celix_steal_ptr(op_name)) != 0) + return -1; + + json_auto_t* path = json_string(path_str); + if (!path) + return -1; + if (json_object_set_new(op, "path", celix_steal_ptr(path)) != 0) + return -1; + + if (json_object_set_new(op, "value", celix_steal_ptr(val)) != 0) + return -1; + + /* on failure the op (and with it the value) is consumed */ + return json_array_append_new(patch, celix_steal_ptr(op)); +} + +int celix_json_patch_remove(json_t* patch, const char* path_str) { + if (!patch || !json_is_array(patch)) + return -1; + + /* op is auto-released on every early return */ + json_auto_t* op = json_object(); + if (!op) + return -1; + + json_auto_t* op_name = json_string("remove"); + if (!op_name) + return -1; + if (json_object_set_new(op, "op", celix_steal_ptr(op_name)) != 0) + return -1; + + json_auto_t* path = json_string(path_str); + if (!path) + return -1; + if (json_object_set_new(op, "path", celix_steal_ptr(path)) != 0) + return -1; + + /* on failure the op is consumed */ + return json_array_append_new(patch, celix_steal_ptr(op)); +} + +void celix_json_patch_truncate(json_t* patch, size_t old_size) { + if (!patch || !json_is_array(patch)) + return; + + while (json_array_size(patch) > old_size) + json_array_remove(patch, json_array_size(patch) - 1); +} + +/* ── Patch application ─────────────────────────────────────────────────── */ + +json_t* celix_json_patch_apply(json_t* original, json_t* patch) { + if (!original || !patch || !json_is_array(patch)) + return NULL; + + /* result is auto-released on the error path */ + json_auto_t* result = json_deep_copy(original); + if (!result) + return NULL; + + size_t n = json_array_size(patch); + for (size_t i = 0; i < n; i++) { + json_t* op_obj = json_array_get(patch, i); + const char* op_type = json_string_value(json_object_get(op_obj, "op")); + const char* path_str = json_string_value(json_object_get(op_obj, "path")); + json_t* value = json_object_get(op_obj, "value"); + + if (!op_type || !path_str) + continue; + + /* Parse the path; ptr is auto-cleared when the loop body exits, + * including on continue and on the error jump below */ + celix_auto(celix_json_pointer_t) ptr; + if (celix_json_pointer_init(&ptr, path_str) != 0) + continue; + + if (strcmp(op_type, "add") == 0 || strcmp(op_type, "replace") == 0) { + if (!value) + continue; + + if (ptr.len == 0) { + /* Root replacement: copy first, then swap the ownership */ + json_decref(result); + result = json_deep_copy(value); + if (!result) + return NULL; + } else { + /* Find parent */ + const char* last = ptr.tokens[ptr.len - 1]; + /* Walk to parent, creating intermediate nodes */ + json_t* parent = result; + for (size_t j = 0; j < ptr.len - 1; j++) { + json_t* child = NULL; + if (json_is_object(parent)) { + child = json_object_get(parent, ptr.tokens[j]); + if (!child) { + child = json_object(); + if (!child) + return NULL; + /* child is consumed by a failed set_new */ + if (json_object_set_new(parent, ptr.tokens[j], child) != 0) + return NULL; + } + } else if (json_is_array(parent)) { + char* end; + long idx = strtol(ptr.tokens[j], &end, 10); + if (*end != '\0') + break; + while ((size_t)idx > json_array_size(parent)) { + json_t* pad = json_null(); + if (!pad) + return NULL; + /* pad is consumed by a failed append */ + if (json_array_append_new(parent, pad) != 0) + return NULL; + } + child = json_array_get(parent, (size_t)idx); + if (!child) { + child = json_object(); + if (!child) + return NULL; + if (json_array_append_new(parent, child) != 0) + return NULL; + } + } + parent = child; + if (!parent) + break; + } + + if (parent && json_is_object(parent)) { + if (strcmp(op_type, "replace") == 0 && json_object_get(parent, last)) { + /* the incref'd copy is consumed by a failed set_new */ + if (json_object_set_new(parent, last, json_incref(value)) != 0) + return NULL; + } else if (strcmp(op_type, "add") == 0) { + /* the incref'd copy is consumed by a failed set_new */ + if (json_object_set_new(parent, last, json_incref(value)) != 0) + return NULL; + } + } else if (parent && json_is_array(parent)) { + char* end; + long idx = strtol(last, &end, 10); + if (*end == '\0' && idx >= 0) { + if (strcmp(op_type, "replace") == 0 && (size_t)idx < json_array_size(parent)) { + json_t* copy = json_deep_copy(value); + if (!copy) + return NULL; + /* copy is consumed by a failed set_new */ + if (json_array_set_new(parent, (size_t)idx, copy) != 0) + return NULL; + } else if (strcmp(op_type, "add") == 0) { + while ((size_t)idx > json_array_size(parent)) { + json_t* pad = json_null(); + if (!pad) + return NULL; + if (json_array_append_new(parent, pad) != 0) + return NULL; + } + if ((size_t)idx == json_array_size(parent)) { + /* the incref'd copy is consumed by a failed append */ + if (json_array_append_new(parent, json_incref(value)) != 0) + return NULL; + } else { + json_t* copy = json_deep_copy(value); + if (!copy) + return NULL; + /* copy is consumed by a failed insert_new */ + if (json_array_insert_new(parent, (size_t)idx, copy) != 0) + return NULL; + } + } + } + } + } + } else if (strcmp(op_type, "remove") == 0) { + if (ptr.len == 0) { + /* Root removal: create the null first, then swap the ownership */ + json_decref(result); + result = json_null(); + if (!result) + return NULL; + } else { + const char* last = ptr.tokens[ptr.len - 1]; + json_t* parent = result; + for (size_t j = 0; j < ptr.len - 1 && parent; j++) { + if (json_is_object(parent)) + parent = json_object_get(parent, ptr.tokens[j]); + else if (json_is_array(parent)) { + char* end; + long idx = strtol(ptr.tokens[j], &end, 10); + parent = (*end == '\0' && idx >= 0) ? json_array_get(parent, (size_t)idx) : NULL; + } else + parent = NULL; + } + if (parent && json_is_object(parent)) + json_object_del(parent, last); + else if (parent && json_is_array(parent)) { + char* end; + long idx = strtol(last, &end, 10); + if (*end == '\0' && idx >= 0 && (size_t)idx < json_array_size(parent)) + json_array_remove(parent, (size_t)idx); + } + } + } + } + + return celix_steal_ptr(result); +} diff --git a/libs/jansson_ext/src/celix_schema.h b/libs/jansson_ext/src/celix_schema.h new file mode 100644 index 000000000..03ebffb27 --- /dev/null +++ b/libs/jansson_ext/src/celix_schema.h @@ -0,0 +1,313 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#ifndef CELIX_CELIX_SCHEMA_H +#define CELIX_CELIX_SCHEMA_H + +#include "celix_jansson_uri.h" +#include "celix_string_hash_map.h" +#include "celix_util.h" +#include +#include +#include + +/* ── Forward declarations ─────────────────────────────────────────────── */ + +typedef struct celix_jansson_schema_root_t celix_jansson_schema_root_t; +typedef struct celix_jansson_schema_node_t celix_jansson_schema_node_t; +typedef struct celix_jansson_error_sink_t celix_jansson_error_sink_t; +typedef struct celix_jansson_validation_context_t celix_jansson_validation_context_t; + +/* ── Schema node kind enumeration ──────────────────────────────────────── */ + +enum celix_jansson_schema_kind_e { + CELIX_JANSSON_SCHEMA_KIND_BOOLEAN, /* true / false schema */ + CELIX_JANSSON_SCHEMA_KIND_TYPE, /* type dispatcher */ + CELIX_JANSSON_SCHEMA_KIND_REF, /* $ref proxy */ + CELIX_JANSSON_SCHEMA_KIND_STRING, /* string constraints */ + CELIX_JANSSON_SCHEMA_KIND_NUMERIC_INT, /* integer constraints */ + CELIX_JANSSON_SCHEMA_KIND_NUMERIC_FLOAT, /* number (float) constraints */ + CELIX_JANSSON_SCHEMA_KIND_NULL, /* null type */ + CELIX_JANSSON_SCHEMA_KIND_BOOLEAN_TYPE, /* boolean type */ + CELIX_JANSSON_SCHEMA_KIND_OBJECT, /* object constraints */ + CELIX_JANSSON_SCHEMA_KIND_ARRAY, /* array constraints */ + CELIX_JANSSON_SCHEMA_KIND_REQUIRED, /* required property list (for array-form dependencies) */ + CELIX_JANSSON_SCHEMA_KIND_NOT, /* "not" combinator */ + CELIX_JANSSON_SCHEMA_KIND_ALL_OF, /* "allOf" combinator */ + CELIX_JANSSON_SCHEMA_KIND_ANY_OF, /* "anyOf" combinator */ + CELIX_JANSSON_SCHEMA_KIND_ONE_OF, /* "oneOf" combinator */ +}; + +/* ── Path stack for recursive validation ───────────────────────────────── */ + +typedef struct celix_jansson_path_t { + char** tokens; + size_t len; + size_t cap; + char* cached; /* lazily computed pointer string */ +} celix_jansson_path_t; + +void celix_jansson_path_init(celix_jansson_path_t* p); +int celix_jansson_path_push(celix_jansson_path_t* p, const char* token); +void celix_jansson_path_pop(celix_jansson_path_t* p); +const char* celix_jansson_path_str(celix_jansson_path_t* p); +void celix_jansson_path_free(celix_jansson_path_t* p); + +/* Enables `celix_auto(celix_jansson_path_t) p;` for scope-based automatic + * cleanup. Safe on zeroed structs (free() frees NULLs and re-zeroes). */ +CELIX_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(celix_jansson_path_t, celix_jansson_path_free) + +/* ── Vtable ────────────────────────────────────────────────────────────── */ + +struct celix_jansson_schema_node_t; + +typedef struct schema_vtable { + int (*validate)(const struct celix_jansson_schema_node_t* self, + json_t* instance, + celix_jansson_path_t* path, + struct celix_jansson_validation_context_t* ctx); + const json_t* (*default_value)(const struct celix_jansson_schema_node_t* self, + celix_jansson_path_t* path, + const json_t* instance, + struct celix_jansson_validation_context_t* ctx); + void (*destroy)(struct celix_jansson_schema_node_t* self); +} schema_vtable; + +/* ── Schema node ───────────────────────────────────────────────────────── */ + +struct celix_jansson_schema_node_t { + const schema_vtable* vtable; + enum celix_jansson_schema_kind_e kind; + unsigned refcount; + celix_jansson_schema_root_t* root; + json_t* default_value; /* owned, or NULL */ + + union { + /* CELIX_JANSSON_SCHEMA_KIND_BOOLEAN */ + struct { + bool value; /* true = always valid */ + } boolean; + + /* CELIX_JANSSON_SCHEMA_KIND_TYPE */ + struct { + /* + * One slot per JSON type. Index: + * 0 = null, 1 = object, 2 = array, 3 = string, + * 4 = boolean, 5 = integer, 6 = real + */ + celix_jansson_schema_node_t* type_slots[7]; + /* enum */ + bool has_enum; + json_t* enum_values; /* array, owned */ + /* const */ + bool has_const; + json_t* const_value; /* owned */ + /* logical combinators (not/allOf/anyOf/oneOf, in order) */ + celix_jansson_schema_node_t** logic; + size_t logic_len; + /* conditional */ + celix_jansson_schema_node_t* if_schema; + celix_jansson_schema_node_t* then_schema; + celix_jansson_schema_node_t* else_schema; + } type_schema; + + /* CELIX_JANSSON_SCHEMA_KIND_REF */ + struct { + char* id; /* the $ref URI string */ + celix_jansson_schema_node_t* target_weak; /* non-owning (registry holds ref) */ + } ref; + + /* CELIX_JANSSON_SCHEMA_KIND_STRING */ + struct { + bool has_min_len, has_max_len; + size_t min_len, max_len; + bool has_pattern; + regex_t pattern; + char* pattern_str; /* original pattern string */ + bool has_format; + char* format; + bool has_content; + char* content_encoding; + char* content_media_type; + } string; + + /* CELIX_JANSSON_SCHEMA_KIND_NUMERIC_INT / CELIX_JANSSON_SCHEMA_KIND_NUMERIC_FLOAT */ + struct { + bool has_max, has_min, has_mult; + bool exclusive_max, exclusive_min; + double multiple_of; /* always double for arithmetic */ + /* Use union for storage; kind determines which is active */ + union { + struct { + json_int_t max; + json_int_t min; + } i; + struct { + double max; + double min; + } f; + } bounds; + } numeric; + + /* CELIX_JANSSON_SCHEMA_KIND_OBJECT */ + struct { + bool has_min_p, has_max_p; + size_t min_p, max_p; + char** required; /* array of required property names */ + size_t required_len; + celix_string_hash_map_t* properties; /* name -> celix_jansson_schema_node_t* (owning ref) */ + /* patternProperties: array of (compiled regex, celix_jansson_schema_node_t*) */ + struct { + regex_t re; + celix_jansson_schema_node_t* sch; + }* pattern_properties; + size_t pp_len; + celix_jansson_schema_node_t* additional_properties; /* or NULL */ + celix_string_hash_map_t* dependencies; /* name -> celix_jansson_schema_node_t* (owning ref, + CELIX_JANSSON_SCHEMA_KIND_REQUIRED or full schema) */ + celix_jansson_schema_node_t* property_names; /* or NULL */ + } object; + + /* CELIX_JANSSON_SCHEMA_KIND_ARRAY */ + struct { + bool has_min_i, has_max_i; + size_t min_items, max_items; + bool unique_items; + /* items: single-schema form */ + celix_jansson_schema_node_t* items_schema; + /* items: tuple form (mutually exclusive with items_schema) */ + celix_jansson_schema_node_t** items; + size_t items_len; + celix_jansson_schema_node_t* additional_items; + celix_jansson_schema_node_t* contains; + } array; + + /* CELIX_JANSSON_SCHEMA_KIND_REQUIRED */ + struct { + char** names; + size_t len; + } required; + + /* CELIX_JANSSON_SCHEMA_KIND_NOT */ + struct { + celix_jansson_schema_node_t* sub; + } not_schema; + + /* CELIX_JANSSON_SCHEMA_KIND_ALL_OF / ANY_OF / ONE_OF */ + struct { + celix_jansson_schema_node_t** items; + size_t len; + } combination; + } u; +}; + +/* ── Reference counting ────────────────────────────────────────────────── */ + +/** Increment refcount. Returns the node (for chaining). */ +celix_jansson_schema_node_t* celix_jansson_schema_ref(celix_jansson_schema_node_t* n); + +/** Decrement refcount. Frees the node (and all children) when zero. */ +void celix_jansson_schema_unref(celix_jansson_schema_node_t* n); + +/* ── Auto cleanup ──────────────────────────────────────────────────────── */ + +/** Enables `celix_autoptr(celix_jansson_schema_node_t)` for scope-based + * ownership of schema nodes (NULL-safe; routes through the vtable destroy). */ +CELIX_DEFINE_AUTOPTR_CLEANUP_FUNC(celix_jansson_schema_node_t, celix_jansson_schema_unref) + +/* ── Error sink (polymorphic error collector) ──────────────────────────── */ + +struct celix_jansson_error_sink_t { + void (*emit)(struct celix_jansson_error_sink_t* sink, const char* path_str, json_t* instance, const char* message); + void (*destroy)(struct celix_jansson_error_sink_t* sink); + void* data; +}; + +/* ── Error entry for collecting sinks ──────────────────────────────────── */ + +typedef struct celix_jansson_error_entry_t { + char* ptr; /* malloc'd JSON pointer string */ + json_t* instance; /* json_incref'd */ + char* message; /* malloc'd error message */ +} celix_jansson_error_entry_t; + +typedef struct celix_jansson_error_list_t { + celix_jansson_error_entry_t* entries; + size_t len; + size_t cap; +} celix_jansson_error_list_t; + +void celix_jansson_error_list_init(celix_jansson_error_list_t* el); +void celix_jansson_error_list_add(celix_jansson_error_list_t* el, const char* ptr, json_t* instance, const char* msg); +void celix_jansson_error_list_clear(celix_jansson_error_list_t* el); + +/* ── Validation context ────────────────────────────────────────────────── */ + +struct celix_jansson_validation_context_t { + celix_jansson_schema_root_t* root; + celix_jansson_error_sink_t* sink; + json_t* patch; /* owned JSON array of {op,path,value} objects */ + celix_jansson_strbuf_t scratch; /* reusable message formatting buffer */ + bool aborted; /* set by abort-on-error sink; checked by validators to stop early */ + int ref_depth; /* guards against infinite recursion through circular $ref */ +}; + +/* ── Schema file (per-location registry entry) ─────────────────────────── */ + +typedef struct celix_jansson_schema_file_t { + celix_string_hash_map_t* schemas; /* fragment string -> celix_jansson_schema_node_t* (owning ref) */ + celix_string_hash_map_t* + unresolved; /* fragment string -> celix_jansson_schema_node_t* (CELIX_JANSSON_SCHEMA_KIND_REF, owning ref) */ + json_t* document; /* original JSON document for this location (owned), or NULL */ + char* base_uri; /* base URI used when compiling this document (owned), or NULL */ + celix_jansson_vec_t retained; /* resolved placeholders kept alive for weak-ref indirection */ +} celix_jansson_schema_file_t; + +/* ── Root schema (the central registry) ────────────────────────────────── */ + +struct celix_jansson_schema_root_t { + celix_string_hash_map_t* files; /* location string -> celix_jansson_schema_file_t* */ + celix_jansson_schema_loader_fn loader; + void* loader_ud; + celix_jansson_schema_format_checker_fn format; + void* format_ud; + celix_jansson_schema_content_checker_fn content; + void* content_ud; + celix_jansson_schema_node_t* root; /* the compiled root, or NULL */ + json_t* original_schema; /* original JSON for JSON-pointer $ref resolution */ +}; + +/* ── Registry functions ────────────────────────────────────────────────── */ + +celix_jansson_schema_file_t* celix_jansson_schema_root_get_or_create_file(celix_jansson_schema_root_t* root, + const char* location); +int celix_jansson_schema_root_insert(celix_jansson_schema_root_t* root, + const celix_jansson_uri_t* uri, + celix_jansson_schema_node_t* sch); +int celix_jansson_schema_root_validate(celix_jansson_schema_root_t* root, + const char* initial_uri, + json_t* instance, + celix_jansson_validation_context_t* ctx); +void celix_jansson_schema_root_destroy(celix_jansson_schema_root_t* root); + +/* ── Type mapping ──────────────────────────────────────────────────────── */ + +/** Map a jansson json_type to the type_slots index (0..6). Returns -1 if unknown. */ +int celix_jansson_type_index(json_t* value); + +#endif /* CELIX_CELIX_SCHEMA_H */ diff --git a/libs/jansson_ext/src/celix_smtp_address_validator.c b/libs/jansson_ext/src/celix_smtp_address_validator.c new file mode 100644 index 000000000..48c23c7f6 --- /dev/null +++ b/libs/jansson_ext/src/celix_smtp_address_validator.c @@ -0,0 +1,402 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "celix_smtp_address_validator.h" + +/* + * Ragel-generated state machine tables. + * + * The _address_trans_keys and _address_cond_actions tables were corrupted in + * the C port of this machine (an 8-byte fragment inserted into trans_keys at + * offset 642, and the EOF actions for final states 196/197 changed from + * no-op to "result = false"). The values here are taken from the working + * C++ original (Gene Hightower's smtp-address-validator.cpp, MIT licensed, + * as vendored in https://github.com/pboettch/json-schema-validator); all + * other tables and the execution loop are unchanged from that source. + * + * Portions of this file were originally derived from Gene Hightower's + * MIT-licensed smtp-address-validator.cpp, available at: + * https://github.com/pboettch/json-schema-validator + */ + +static const signed char _address_actions[] = {0, 1, 0, 1, 1, 0}; + +static const short _address_key_offsets[] = { + 0, 0, 24, 26, 50, 52, 54, 56, 58, 60, 62, 86, 103, 105, 107, 109, 111, 113, 115, + 117, 134, 150, 161, 168, 176, 180, 181, 190, 195, 196, 201, 202, 207, 210, 213, 219, 222, 225, + 228, 234, 237, 240, 243, 249, 252, 261, 270, 282, 293, 302, 311, 320, 328, 345, 353, 360, 367, + 368, 375, 382, 389, 396, 397, 404, 411, 418, 425, 426, 433, 440, 447, 454, 455, 462, 469, 476, + 483, 484, 491, 498, 505, 512, 513, 523, 531, 538, 545, 546, 552, 559, 566, 573, 581, 589, 597, + 608, 618, 626, 634, 641, 649, 657, 665, 667, 673, 681, 689, 697, 699, 705, 713, 721, 729, 731, + 737, 745, 753, 761, 763, 769, 777, 785, 793, 795, 802, 812, 821, 829, 837, 839, 848, 857, 865, + 873, 875, 884, 893, 901, 909, 911, 920, 929, 937, 945, 947, 956, 965, 974, 983, 992, 1004, 1015, + 1024, 1033, 1042, 1051, 1060, 1072, 1083, 1092, 1101, 1109, 1118, 1127, 1136, 1148, 1159, 1168, 1177, 1185, 1194, + 1203, 1212, 1224, 1235, 1244, 1253, 1261, 1270, 1279, 1288, 1300, 1311, 1320, 1329, 1337, 1339, 1353, 1355, 1357, + 1359, 1361, 1363, 1365, 1367, 1368, 1370, 1388, 0}; + +static const signed char _address_trans_keys[] = { + -32, -19, -16, -12, 34, 45, 61, 63, -62, -33, -31, -17, -15, -13, 33, 39, 42, 43, 47, 57, + 65, 90, 94, 126, -128, -65, -32, -19, -16, -12, 33, 46, 61, 64, -62, -33, -31, -17, -15, -13, + 35, 39, 42, 43, 45, 57, 63, 90, 94, 126, -96, -65, -128, -65, -128, -97, -112, -65, -128, -65, + -128, -113, -32, -19, -16, -12, 33, 45, 61, 63, -62, -33, -31, -17, -15, -13, 35, 39, 42, 43, + 47, 57, 65, 90, 94, 126, -32, -19, -16, -12, 91, -62, -33, -31, -17, -15, -13, 48, 57, 65, + 90, 97, 122, -128, -65, -96, -65, -128, -65, -128, -97, -112, -65, -128, -65, -128, -113, -32, -19, -16, + -12, 45, -62, -33, -31, -17, -15, -13, 48, 57, 65, 90, 97, 122, -32, -19, -16, -12, -62, -33, + -31, -17, -15, -13, 48, 57, 65, 90, 97, 122, 45, 48, 49, 50, 73, 51, 57, 65, 90, 97, + 122, 45, 48, 57, 65, 90, 97, 122, 45, 58, 48, 57, 65, 90, 97, 122, 33, 90, 94, 126, + 93, 45, 46, 58, 48, 57, 65, 90, 97, 122, 48, 49, 50, 51, 57, 46, 48, 49, 50, 51, + 57, 46, 48, 49, 50, 51, 57, 93, 48, 57, 93, 48, 57, 53, 93, 48, 52, 54, 57, 93, + 48, 53, 46, 48, 57, 46, 48, 57, 46, 53, 48, 52, 54, 57, 46, 48, 53, 46, 48, 57, + 46, 48, 57, 46, 53, 48, 52, 54, 57, 46, 48, 53, 45, 46, 58, 48, 57, 65, 90, 97, + 122, 45, 46, 58, 48, 57, 65, 90, 97, 122, 45, 46, 53, 58, 48, 52, 54, 57, 65, 90, + 97, 122, 45, 46, 58, 48, 53, 54, 57, 65, 90, 97, 122, 45, 58, 80, 48, 57, 65, 90, + 97, 122, 45, 58, 118, 48, 57, 65, 90, 97, 122, 45, 54, 58, 48, 57, 65, 90, 97, 122, + 45, 58, 48, 57, 65, 90, 97, 122, 58, 33, 47, 48, 57, 59, 64, 65, 70, 71, 90, 94, + 96, 97, 102, 103, 126, 58, 93, 48, 57, 65, 70, 97, 102, 58, 48, 57, 65, 70, 97, 102, + 58, 48, 57, 65, 70, 97, 102, 58, 58, 48, 57, 65, 70, 97, 102, 58, 48, 57, 65, 70, + 97, 102, 58, 48, 57, 65, 70, 97, 102, 58, 48, 57, 65, 70, 97, 102, 58, 58, 48, 57, + 65, 70, 97, 102, 58, 48, 57, 65, 70, 97, 102, 58, 48, 57, 65, 70, 97, 102, 58, 48, + 57, 65, 70, 97, 102, 58, 58, 48, 57, 65, 70, 97, 102, 58, 48, 57, 65, 70, 97, 102, + 58, 48, 57, 65, 70, 97, 102, 58, 48, 57, 65, 70, 97, 102, 58, 58, 48, 57, 65, 70, + 97, 102, 58, 48, 57, 65, 70, 97, 102, 58, 48, 57, 65, 70, 97, 102, 58, 48, 57, 65, + 70, 97, 102, 58, 58, 48, 57, 65, 70, 97, 102, 58, 48, 57, 65, 70, 97, 102, 58, 48, + 57, 65, 70, 97, 102, 58, 48, 57, 65, 70, 97, 102, 58, 48, 49, 50, 58, 51, 57, 65, + 70, 97, 102, 46, 58, 48, 57, 65, 70, 97, 102, 58, 48, 57, 65, 70, 97, 102, 58, 48, + 57, 65, 70, 97, 102, 58, 48, 57, 65, 70, 97, 102, 93, 48, 57, 65, 70, 97, 102, 93, + 48, 57, 65, 70, 97, 102, 93, 48, 57, 65, 70, 97, 102, 46, 58, 48, 57, 65, 70, 97, + 102, 46, 58, 48, 57, 65, 70, 97, 102, 46, 58, 48, 57, 65, 70, 97, 102, 46, 53, 58, + 48, 52, 54, 57, 65, 70, 97, 102, 46, 58, 48, 53, 54, 57, 65, 70, 97, 102, 46, 58, + 48, 57, 65, 70, 97, 102, 46, 58, 48, 57, 65, 70, 97, 102, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, + 57, 65, 70, 97, 102, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, + 57, 65, 70, 97, 102, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, + 57, 65, 70, 97, 102, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, + 57, 65, 70, 97, 102, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 58, 48, 57, 65, 70, + 97, 102, 48, 49, 50, 93, 51, 57, 65, 70, 97, 102, 46, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, + 49, 50, 51, 57, 65, 70, 97, 102, 46, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, + 57, 65, 70, 97, 102, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, 49, 50, 51, 57, + 65, 70, 97, 102, 46, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, 49, 50, 51, 57, 65, 70, 97, 102, + 46, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, + 57, 65, 70, 97, 102, 58, 93, 48, 49, 50, 51, 57, 65, 70, 97, 102, 46, 58, 93, 48, + 57, 65, 70, 97, 102, 46, 58, 93, 48, 57, 65, 70, 97, 102, 46, 58, 93, 48, 57, 65, + 70, 97, 102, 46, 58, 93, 48, 57, 65, 70, 97, 102, 46, 53, 58, 93, 48, 52, 54, 57, + 65, 70, 97, 102, 46, 58, 93, 48, 53, 54, 57, 65, 70, 97, 102, 46, 58, 93, 48, 57, + 65, 70, 97, 102, 46, 58, 93, 48, 57, 65, 70, 97, 102, 46, 58, 93, 48, 57, 65, 70, + 97, 102, 46, 58, 93, 48, 57, 65, 70, 97, 102, 46, 58, 93, 48, 57, 65, 70, 97, 102, + 46, 53, 58, 93, 48, 52, 54, 57, 65, 70, 97, 102, 46, 58, 93, 48, 53, 54, 57, 65, + 70, 97, 102, 46, 58, 93, 48, 57, 65, 70, 97, 102, 46, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, 102, 46, 58, 93, 48, 57, 65, 70, 97, 102, 46, 58, + 93, 48, 57, 65, 70, 97, 102, 46, 58, 93, 48, 57, 65, 70, 97, 102, 46, 53, 58, 93, + 48, 52, 54, 57, 65, 70, 97, 102, 46, 58, 93, 48, 53, 54, 57, 65, 70, 97, 102, 46, + 58, 93, 48, 57, 65, 70, 97, 102, 46, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, + 57, 65, 70, 97, 102, 46, 58, 93, 48, 57, 65, 70, 97, 102, 46, 58, 93, 48, 57, 65, + 70, 97, 102, 46, 58, 93, 48, 57, 65, 70, 97, 102, 46, 53, 58, 93, 48, 52, 54, 57, + 65, 70, 97, 102, 46, 58, 93, 48, 53, 54, 57, 65, 70, 97, 102, 46, 58, 93, 48, 57, + 65, 70, 97, 102, 46, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, 57, 65, 70, 97, + 102, 46, 58, 93, 48, 57, 65, 70, 97, 102, 46, 58, 93, 48, 57, 65, 70, 97, 102, 46, + 58, 93, 48, 57, 65, 70, 97, 102, 46, 53, 58, 93, 48, 52, 54, 57, 65, 70, 97, 102, + 46, 58, 93, 48, 53, 54, 57, 65, 70, 97, 102, 46, 58, 93, 48, 57, 65, 70, 97, 102, + 46, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, 48, 57, 65, 70, 97, 102, 58, 93, -32, + -19, -16, -12, 34, 92, -62, -33, -31, -17, -15, -13, 32, 126, -128, -65, -96, -65, -128, -65, -128, + -97, -112, -65, -128, -65, -128, -113, 64, 32, 126, -32, -19, -16, -12, 45, 46, -62, -33, -31, -17, + -15, -13, 48, 57, 65, 90, 97, 122, 0}; + +static const signed char _address_single_lengths[] = { + 0, 8, 0, 8, 0, 0, 0, 0, 0, 0, 8, 5, 0, 0, 0, 0, 0, 0, 0, 5, 4, 5, 1, 2, 0, 1, 3, 3, 1, 3, 1, 3, 1, 1, + 2, 1, 1, 1, 2, 1, 1, 1, 2, 1, 3, 3, 4, 3, 3, 3, 3, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 2, 1, 1, 1, 0, 1, 1, 1, 2, 2, 2, 3, 2, 2, 2, 1, 2, 2, 2, + 2, 0, 2, 2, 2, 2, 0, 2, 2, 2, 2, 0, 2, 2, 2, 2, 0, 2, 2, 2, 2, 1, 4, 3, 2, 2, 2, 3, 3, 2, 2, 2, 3, 3, + 2, 2, 2, 3, 3, 2, 2, 2, 3, 3, 3, 3, 3, 4, 3, 3, 3, 3, 3, 3, 4, 3, 3, 3, 2, 3, 3, 3, 4, 3, 3, 3, 2, 3, + 3, 3, 4, 3, 3, 3, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 6, 0, 0, 0, 0, 0, 0, 0, 1, 0, 6, 0, 0}; + +static const signed char _address_range_lengths[] = { + 0, 8, 1, 8, 1, 1, 1, 1, 1, 1, 8, 6, 1, 1, 1, 1, 1, 1, 1, 6, 6, 3, 3, 3, 2, 0, 3, 1, 0, 1, 0, 1, 1, 1, + 2, 1, 1, 1, 2, 1, 1, 1, 2, 1, 3, 3, 4, 4, 3, 3, 3, 3, 8, 3, 3, 3, 0, 3, 3, 3, 3, 0, 3, 3, 3, 3, 0, 3, + 3, 3, 3, 0, 3, 3, 3, 3, 0, 3, 3, 3, 3, 0, 3, 3, 3, 3, 0, 3, 3, 3, 3, 3, 3, 3, 4, 4, 3, 3, 3, 3, 3, 3, + 0, 3, 3, 3, 3, 0, 3, 3, 3, 3, 0, 3, 3, 3, 3, 0, 3, 3, 3, 3, 0, 3, 3, 3, 3, 3, 0, 3, 3, 3, 3, 0, 3, 3, + 3, 3, 0, 3, 3, 3, 3, 0, 3, 3, 3, 3, 3, 4, 4, 3, 3, 3, 3, 3, 4, 4, 3, 3, 3, 3, 3, 3, 4, 4, 3, 3, 3, 3, + 3, 3, 4, 4, 3, 3, 3, 3, 3, 3, 4, 4, 3, 3, 3, 0, 4, 1, 1, 1, 1, 1, 1, 1, 0, 1, 6, 0, 0}; + +static const short _address_index_offsets[] = { + 0, 0, 17, 19, 36, 38, 40, 42, 44, 46, 48, 65, 77, 79, 81, 83, 85, 87, 89, + 91, 103, 114, 123, 128, 134, 137, 139, 146, 151, 153, 158, 160, 165, 168, 171, 176, 179, 182, + 185, 190, 193, 196, 199, 204, 207, 214, 221, 230, 238, 245, 252, 259, 265, 275, 281, 286, 291, + 293, 298, 303, 308, 313, 315, 320, 325, 330, 335, 337, 342, 347, 352, 357, 359, 364, 369, 374, + 379, 381, 386, 391, 396, 401, 403, 411, 417, 422, 427, 429, 433, 438, 443, 448, 454, 460, 466, + 474, 481, 487, 493, 498, 504, 510, 516, 519, 523, 529, 535, 541, 544, 548, 554, 560, 566, 569, + 573, 579, 585, 591, 594, 598, 604, 610, 616, 619, 624, 632, 639, 645, 651, 654, 661, 668, 674, + 680, 683, 690, 697, 703, 709, 712, 719, 726, 732, 738, 741, 748, 755, 762, 769, 776, 785, 793, + 800, 807, 814, 821, 828, 837, 845, 852, 859, 865, 872, 879, 886, 895, 903, 910, 917, 923, 930, + 937, 944, 953, 961, 968, 975, 981, 988, 995, 1002, 1011, 1019, 1026, 1033, 1039, 1042, 1053, 1055, 1057, + 1059, 1061, 1063, 1065, 1067, 1069, 1071, 1084, 0}; + +static const short _address_cond_targs[] = { + 4, 6, 7, 9, 186, 3, 3, 3, 2, 5, 8, 3, 3, 3, 3, 3, 0, 3, 0, 4, 6, 7, 9, + 3, 10, 3, 11, 2, 5, 8, 3, 3, 3, 3, 3, 0, 2, 0, 2, 0, 2, 0, 5, 0, 5, 0, + 5, 0, 4, 6, 7, 9, 3, 3, 3, 3, 2, 5, 8, 3, 3, 3, 3, 3, 0, 13, 15, 16, 18, + 21, 12, 14, 17, 196, 196, 196, 0, 196, 0, 12, 0, 12, 0, 12, 0, 14, 0, 14, 0, 14, 0, 13, + 15, 16, 18, 19, 12, 14, 17, 196, 196, 196, 0, 13, 15, 16, 18, 12, 14, 17, 196, 196, 196, 0, 22, + 26, 44, 46, 48, 45, 23, 23, 0, 22, 23, 23, 23, 0, 22, 24, 23, 23, 23, 0, 25, 25, 0, 197, + 0, 22, 27, 24, 23, 23, 23, 0, 28, 40, 42, 41, 0, 29, 0, 30, 36, 38, 37, 0, 31, 0, 25, + 32, 34, 33, 0, 197, 33, 0, 197, 25, 0, 35, 197, 33, 25, 0, 197, 25, 0, 31, 37, 0, 31, 30, + 0, 31, 39, 37, 30, 0, 31, 30, 0, 29, 41, 0, 29, 28, 0, 29, 43, 41, 28, 0, 29, 28, 0, + 22, 27, 24, 45, 23, 23, 0, 22, 27, 24, 26, 23, 23, 0, 22, 27, 47, 24, 45, 26, 23, 23, 0, + 22, 27, 24, 26, 23, 23, 23, 0, 22, 24, 49, 23, 23, 23, 0, 22, 24, 50, 23, 23, 23, 0, 22, + 51, 24, 23, 23, 23, 0, 22, 52, 23, 23, 23, 0, 185, 25, 53, 25, 53, 25, 25, 53, 25, 0, 57, + 197, 54, 54, 54, 0, 57, 55, 55, 55, 0, 57, 56, 56, 56, 0, 57, 0, 124, 58, 58, 58, 0, 62, + 59, 59, 59, 0, 62, 60, 60, 60, 0, 62, 61, 61, 61, 0, 62, 0, 124, 63, 63, 63, 0, 67, 64, + 64, 64, 0, 67, 65, 65, 65, 0, 67, 66, 66, 66, 0, 67, 0, 124, 68, 68, 68, 0, 72, 69, 69, + 69, 0, 72, 70, 70, 70, 0, 72, 71, 71, 71, 0, 72, 0, 124, 73, 73, 73, 0, 77, 74, 74, 74, + 0, 77, 75, 75, 75, 0, 77, 76, 76, 76, 0, 77, 0, 98, 78, 78, 78, 0, 82, 79, 79, 79, 0, + 82, 80, 80, 80, 0, 82, 81, 81, 81, 0, 82, 0, 83, 91, 94, 98, 97, 123, 123, 0, 27, 87, 84, + 84, 84, 0, 87, 85, 85, 85, 0, 87, 86, 86, 86, 0, 87, 0, 88, 88, 88, 0, 197, 89, 89, 89, + 0, 197, 90, 90, 90, 0, 197, 25, 25, 25, 0, 27, 87, 92, 84, 84, 0, 27, 87, 93, 85, 85, 0, + 27, 87, 86, 86, 86, 0, 27, 95, 87, 92, 96, 84, 84, 0, 27, 87, 93, 85, 85, 85, 0, 27, 87, + 85, 85, 85, 0, 27, 87, 96, 84, 84, 0, 197, 99, 99, 99, 0, 103, 197, 100, 100, 100, 0, 103, 197, + 101, 101, 101, 0, 103, 197, 102, 102, 102, 0, 103, 197, 0, 104, 104, 104, 0, 108, 197, 105, 105, 105, 0, + 108, 197, 106, 106, 106, 0, 108, 197, 107, 107, 107, 0, 108, 197, 0, 109, 109, 109, 0, 113, 197, 110, 110, + 110, 0, 113, 197, 111, 111, 111, 0, 113, 197, 112, 112, 112, 0, 113, 197, 0, 114, 114, 114, 0, 118, 197, + 115, 115, 115, 0, 118, 197, 116, 116, 116, 0, 118, 197, 117, 117, 117, 0, 118, 197, 0, 119, 119, 119, 0, + 87, 197, 120, 120, 120, 0, 87, 197, 121, 121, 121, 0, 87, 197, 122, 122, 122, 0, 87, 197, 0, 87, 84, + 84, 84, 0, 125, 177, 180, 197, 183, 184, 184, 0, 27, 129, 197, 126, 126, 126, 0, 129, 197, 127, 127, 127, + 0, 129, 197, 128, 128, 128, 0, 129, 197, 0, 130, 169, 172, 175, 176, 176, 0, 27, 134, 197, 131, 131, 131, + 0, 134, 197, 132, 132, 132, 0, 134, 197, 133, 133, 133, 0, 134, 197, 0, 135, 161, 164, 167, 168, 168, 0, + 27, 139, 197, 136, 136, 136, 0, 139, 197, 137, 137, 137, 0, 139, 197, 138, 138, 138, 0, 139, 197, 0, 140, + 153, 156, 159, 160, 160, 0, 27, 144, 197, 141, 141, 141, 0, 144, 197, 142, 142, 142, 0, 144, 197, 143, 143, + 143, 0, 144, 197, 0, 145, 146, 149, 152, 119, 119, 0, 27, 87, 197, 120, 120, 120, 0, 27, 87, 197, 147, + 120, 120, 0, 27, 87, 197, 148, 121, 121, 0, 27, 87, 197, 122, 122, 122, 0, 27, 150, 87, 197, 147, 151, + 120, 120, 0, 27, 87, 197, 148, 121, 121, 121, 0, 27, 87, 197, 121, 121, 121, 0, 27, 87, 197, 151, 120, + 120, 0, 27, 144, 197, 154, 141, 141, 0, 27, 144, 197, 155, 142, 142, 0, 27, 144, 197, 143, 143, 143, 0, + 27, 157, 144, 197, 154, 158, 141, 141, 0, 27, 144, 197, 155, 142, 142, 142, 0, 27, 144, 197, 142, 142, 142, + 0, 27, 144, 197, 158, 141, 141, 0, 144, 197, 141, 141, 141, 0, 27, 139, 197, 162, 136, 136, 0, 27, 139, + 197, 163, 137, 137, 0, 27, 139, 197, 138, 138, 138, 0, 27, 165, 139, 197, 162, 166, 136, 136, 0, 27, 139, + 197, 163, 137, 137, 137, 0, 27, 139, 197, 137, 137, 137, 0, 27, 139, 197, 166, 136, 136, 0, 139, 197, 136, + 136, 136, 0, 27, 134, 197, 170, 131, 131, 0, 27, 134, 197, 171, 132, 132, 0, 27, 134, 197, 133, 133, 133, + 0, 27, 173, 134, 197, 170, 174, 131, 131, 0, 27, 134, 197, 171, 132, 132, 132, 0, 27, 134, 197, 132, 132, + 132, 0, 27, 134, 197, 174, 131, 131, 0, 134, 197, 131, 131, 131, 0, 27, 129, 197, 178, 126, 126, 0, 27, + 129, 197, 179, 127, 127, 0, 27, 129, 197, 128, 128, 128, 0, 27, 181, 129, 197, 178, 182, 126, 126, 0, 27, + 129, 197, 179, 127, 127, 127, 0, 27, 129, 197, 127, 127, 127, 0, 27, 129, 197, 182, 126, 126, 0, 129, 197, + 126, 126, 126, 0, 124, 197, 0, 188, 190, 191, 193, 194, 195, 187, 189, 192, 186, 0, 186, 0, 187, 0, 187, + 0, 187, 0, 189, 0, 189, 0, 189, 0, 11, 0, 186, 0, 13, 15, 16, 18, 19, 20, 12, 14, 17, 196, + 196, 196, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, + 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, + 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, + 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, + 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, + 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, + 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, + 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 0}; + +static const signed char _address_cond_actions[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 3, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 3, 0, 3, + 0, 3, 0, 3, 0, 3, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 3, 1, 3, 0, + 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 1, 3, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 3, 0, 0, 0, 0, 0, 0, + 0, 0, 3, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 3, 0, 0, 3, 1, 3, 0, + 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 3, 0, 3, 0, 0, 0, 0, 3, 0, 3, + 0, 0, 0, 0, 3, 1, 0, 3, 1, 0, 3, 0, 1, 0, 0, 3, 1, 0, 3, 0, + 0, 3, 0, 0, 3, 0, 0, 0, 0, 3, 0, 0, 3, 0, 0, 3, 0, 0, 3, 0, + 0, 0, 0, 3, 0, 0, 3, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, + 3, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, + 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 3, 0, + 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 1, 0, 0, 0, + 3, 0, 0, 0, 0, 3, 0, 0, 0, 0, 3, 0, 3, 0, 0, 0, 0, 3, 0, 0, + 0, 0, 3, 0, 0, 0, 0, 3, 0, 0, 0, 0, 3, 0, 3, 0, 0, 0, 0, 3, + 0, 0, 0, 0, 3, 0, 0, 0, 0, 3, 0, 0, 0, 0, 3, 0, 3, 0, 0, 0, + 0, 3, 0, 0, 0, 0, 3, 0, 0, 0, 0, 3, 0, 0, 0, 0, 3, 0, 3, 0, + 0, 0, 0, 3, 0, 0, 0, 0, 3, 0, 0, 0, 0, 3, 0, 0, 0, 0, 3, 0, + 3, 0, 0, 0, 0, 3, 0, 0, 0, 0, 3, 0, 0, 0, 0, 3, 0, 0, 0, 0, + 3, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 3, 0, 0, 0, + 0, 3, 0, 0, 0, 0, 3, 0, 3, 0, 0, 0, 3, 1, 0, 0, 0, 3, 1, 0, + 0, 0, 3, 1, 0, 0, 0, 3, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 3, + 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, + 3, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 3, 1, 0, 0, 0, 3, 0, 1, + 0, 0, 0, 3, 0, 1, 0, 0, 0, 3, 0, 1, 0, 0, 0, 3, 0, 1, 3, 0, + 0, 0, 3, 0, 1, 0, 0, 0, 3, 0, 1, 0, 0, 0, 3, 0, 1, 0, 0, 0, + 3, 0, 1, 3, 0, 0, 0, 3, 0, 1, 0, 0, 0, 3, 0, 1, 0, 0, 0, 3, + 0, 1, 0, 0, 0, 3, 0, 1, 3, 0, 0, 0, 3, 0, 1, 0, 0, 0, 3, 0, + 1, 0, 0, 0, 3, 0, 1, 0, 0, 0, 3, 0, 1, 3, 0, 0, 0, 3, 0, 1, + 0, 0, 0, 3, 0, 1, 0, 0, 0, 3, 0, 1, 0, 0, 0, 3, 0, 1, 3, 0, + 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0, 3, 0, + 1, 0, 0, 0, 3, 0, 1, 0, 0, 0, 3, 0, 1, 3, 0, 0, 0, 0, 0, 0, + 3, 0, 0, 1, 0, 0, 0, 3, 0, 1, 0, 0, 0, 3, 0, 1, 0, 0, 0, 3, + 0, 1, 3, 0, 0, 0, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0, 3, 0, 1, 0, + 0, 0, 3, 0, 1, 0, 0, 0, 3, 0, 1, 3, 0, 0, 0, 0, 0, 0, 3, 0, + 0, 1, 0, 0, 0, 3, 0, 1, 0, 0, 0, 3, 0, 1, 0, 0, 0, 3, 0, 1, + 3, 0, 0, 0, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0, 3, 0, 0, 1, 0, 0, + 0, 3, 0, 0, 1, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 1, + 0, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0, 3, + 0, 0, 1, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0, + 3, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 0, 3, 0, 0, 1, + 0, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0, 3, 0, + 1, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0, 3, 0, + 0, 1, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 0, 3, 0, 0, 1, 0, 0, + 0, 0, 3, 0, 0, 1, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0, 3, 0, 1, 0, + 0, 0, 3, 0, 0, 1, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0, 3, 0, 0, 1, + 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0, 0, + 3, 0, 0, 1, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0, 3, 0, 1, 0, 0, 0, + 3, 0, 0, 1, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0, 3, 0, 0, 1, 0, 0, + 0, 3, 0, 0, 0, 1, 0, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0, 0, 3, 0, + 0, 1, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0, 3, 0, 1, 0, 0, 0, 3, 0, + 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 3, 0, 3, 0, 3, 0, + 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 3, 3, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 0, 0, 0}; + +static const short _address_eof_trans[] = { + 1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, 1094, 1095, 1096, 1097, 1098, 1099, 1100, 1101, 1102, 1103, 1104, + 1105, 1106, 1107, 1108, 1109, 1110, 1111, 1112, 1113, 1114, 1115, 1116, 1117, 1118, 1119, 1120, 1121, 1122, 1123, + 1124, 1125, 1126, 1127, 1128, 1129, 1130, 1131, 1132, 1133, 1134, 1135, 1136, 1137, 1138, 1139, 1140, 1141, 1142, + 1143, 1144, 1145, 1146, 1147, 1148, 1149, 1150, 1151, 1152, 1153, 1154, 1155, 1156, 1157, 1158, 1159, 1160, 1161, + 1162, 1163, 1164, 1165, 1166, 1167, 1168, 1169, 1170, 1171, 1172, 1173, 1174, 1175, 1176, 1177, 1178, 1179, 1180, + 1181, 1182, 1183, 1184, 1185, 1186, 1187, 1188, 1189, 1190, 1191, 1192, 1193, 1194, 1195, 1196, 1197, 1198, 1199, + 1200, 1201, 1202, 1203, 1204, 1205, 1206, 1207, 1208, 1209, 1210, 1211, 1212, 1213, 1214, 1215, 1216, 1217, 1218, + 1219, 1220, 1221, 1222, 1223, 1224, 1225, 1226, 1227, 1228, 1229, 1230, 1231, 1232, 1233, 1234, 1235, 1236, 1237, + 1238, 1239, 1240, 1241, 1242, 1243, 1244, 1245, 1246, 1247, 1248, 1249, 1250, 1251, 1252, 1253, 1254, 1255, 1256, + 1257, 1258, 1259, 1260, 1261, 1262, 1263, 1264, 1265, 1266, 1267, 1268, 1269, 1270, 1271, 1272, 1273, 1274, 1275, + 1276, 1277, 1278, 1279, 1280, 1281, 1282, 1283, 0}; + +static const int address_start = 1; + +bool celix_jansson_smtp_is_address(const char* p, const char* pe) { + int cs = 0; + bool result = false; + + { + cs = (int)address_start; + } + { + int _klen; + unsigned int _trans = 0; + const signed char* _keys; + const signed char* _acts; + unsigned int _nacts; + _resume: {} + if (p == pe) { + if (_address_eof_trans[cs] > 0) { + _trans = (unsigned int)_address_eof_trans[cs] - 1; + } + } else { + _keys = (_address_trans_keys + (_address_key_offsets[cs])); + _trans = (unsigned int)_address_index_offsets[cs]; + + _klen = (int)_address_single_lengths[cs]; + if (_klen > 0) { + const signed char* _lower = _keys; + const signed char* _upper = _keys + _klen - 1; + const signed char* _mid; + while (1) { + if (_upper < _lower) { + _keys += _klen; + _trans += (unsigned int)_klen; + break; + } + + _mid = _lower + ((_upper - _lower) >> 1); + if (((signed char)*(p)) < (*(_mid))) + _upper = _mid - 1; + else if (((signed char)*(p)) > (*(_mid))) + _lower = _mid + 1; + else { + _trans += (unsigned int)(_mid - _keys); + goto _match; + } + } + } + + _klen = (int)_address_range_lengths[cs]; + if (_klen > 0) { + const signed char* _lower = _keys; + const signed char* _upper = _keys + (_klen << 1) - 2; + const signed char* _mid; + while (1) { + if (_upper < _lower) { + _trans += (unsigned int)_klen; + break; + } + + _mid = _lower + (((_upper - _lower) >> 1) & ~1); + if (((signed char)*(p)) < (*(_mid))) + _upper = _mid - 2; + else if (((signed char)*(p)) > (*(_mid + 1))) + _lower = _mid + 2; + else { + _trans += (unsigned int)((_mid - _keys) >> 1); + break; + } + } + } + + _match: {} + } + cs = (int)_address_cond_targs[_trans]; + + if (_address_cond_actions[_trans] != 0) { + _acts = (_address_actions + (_address_cond_actions[_trans])); + _nacts = (unsigned int)(*(_acts)); + _acts += 1; + while (_nacts > 0) { + switch ((*(_acts))) { + case 0: { + { + result = true; + } + break; + } + case 1: { + { + result = false; + } + break; + } + } + _nacts -= 1; + _acts += 1; + } + } + + if (p == pe) { + if (cs >= 196) + goto _out; + } else { + if (cs != 0) { + p += 1; + goto _resume; + } + } + _out: {} + } + return result; +} diff --git a/libs/jansson_ext/src/celix_smtp_address_validator.h b/libs/jansson_ext/src/celix_smtp_address_validator.h new file mode 100644 index 000000000..239c41e04 --- /dev/null +++ b/libs/jansson_ext/src/celix_smtp_address_validator.h @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#ifndef CELIX_CELIX_SMTP_ADDRESS_VALIDATOR_H +#define CELIX_CELIX_SMTP_ADDRESS_VALIDATOR_H + +#include + +/** + * Validate an email address per RFC 5321/5322 grammar. + * + * @param p Pointer to start of address string + * @param pe Pointer to end of address string (one past last char) + * @return true if the address is valid + */ +bool celix_jansson_smtp_is_address(const char* p, const char* pe); + +#endif /* CELIX_CELIX_SMTP_ADDRESS_VALIDATOR_H */ diff --git a/libs/jansson_ext/src/celix_string_format_check.c b/libs/jansson_ext/src/celix_string_format_check.c new file mode 100644 index 000000000..44f179735 --- /dev/null +++ b/libs/jansson_ext/src/celix_string_format_check.c @@ -0,0 +1,564 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "celix_string_format_check.h" +#include "celix_smtp_address_validator.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ── RFC 3339 Date-Time ────────────────────────────────────────────────── */ + +static int is_leap_year(int year) { return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); } + +static int days_in_month(int year, int month) { + static const int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + /* Caller (check_date_part) guarantees 1 <= month <= 12 */ + assert(month >= 1 && month <= 12); + if (month == 2 && is_leap_year(year)) + return 29; + return days[month - 1]; +} + +static int check_date_part(const char* s) { + /* YYYY-MM-DD — must be exactly 10 chars with zero-padded month/day */ + if (!s || strlen(s) != 10) + return -1; + if (s[4] != '-' || s[7] != '-') + return -1; + /* Verify all other chars are digits */ + for (int i = 0; i < 10; i++) { + if (i == 4 || i == 7) + continue; + if (!isdigit((unsigned char)s[i])) + return -1; + } + int year, month, day; + /* All non-separator chars were validated as digits above, so the parse always succeeds */ + int n = sscanf(s, "%4d-%2d-%2d", &year, &month, &day); + assert(n == 3); + (void)n; + if (month < 1 || month > 12) + return -1; + if (day < 1 || day > days_in_month(year, month)) + return -1; + return 0; +} + +static int parse_time_part(const char* s, int* out_hour, int* out_min, int* out_sec) { + /* HH:MM:SS[.fraction] — parse components, return 0 on success */ + int hour, min, sec; + double frac = 0.0; + int n = 0; + + if (strchr(s, '.')) { + n = sscanf(s, "%2d:%2d:%2d.%lf", &hour, &min, &sec, &frac); + } else { + n = sscanf(s, "%2d:%2d:%2d", &hour, &min, &sec); + } + + if (n < 3) + return -1; + if (hour < 0 || hour > 23) + return -1; + if (min < 0 || min > 59) + return -1; + if (sec < 0 || sec > 60) + return -1; + + *out_hour = hour; + *out_min = min; + *out_sec = sec; + return 0; +} + +static int check_leap_second(int hour, int min, int sec, int offset_hour, int offset_min) { + /* Per RFC 3339, a leap second (sec == 60) is only valid when the + * UTC-normalized time equals 23:59:60. Normalize local time to UTC + * day-minutes and check. */ + if (sec != 60) + return 0; + + int day_minutes = hour * 60 + min - (offset_hour * 60 + offset_min); + /* Wrap into [0, 1440) */ + while (day_minutes < 0) + day_minutes += 24 * 60; + day_minutes %= (24 * 60); + + int utc_hour = day_minutes / 60; + int utc_min = day_minutes % 60; + return (utc_hour == 23 && utc_min == 59) ? 0 : -1; +} + +static int parse_timezone(const char* s, int* out_hh, int* out_mm) { + /* Callers pass the result of strpbrk(s, "+-Zz"), so s is non-NULL and non-empty */ + assert(s != NULL && *s != '\0'); + if (*s == 'Z' || *s == 'z') { + *out_hh = 0; + *out_mm = 0; + return 0; + } + /* ±HH:MM — s[0] is '+' or '-' (Z/z handled above) */ + int hh, mm; + char sign; + if (sscanf(s, "%c%2d:%2d", &sign, &hh, &mm) != 3) + return -1; + assert(sign == '+' || sign == '-'); + if (hh < 0 || hh > 23) + return -1; + if (mm < 0 || mm > 59) + return -1; + if (sign == '-') { + hh = -hh; + mm = -mm; + } + *out_hh = hh; + *out_mm = mm; + return 0; +} + +int celix_jansson_check_date_time(const char* value) { + /* Format: YYYY-MM-DD T HH:MM:SS[.fraction] (Z|±HH:MM) */ + const char* t = strpbrk(value, "Tt"); + if (!t) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + + size_t date_len = (size_t)(t - value); + if (date_len >= 16) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + + /* Check date part */ + char date_part[16] = {0}; + memcpy(date_part, value, date_len); + if (check_date_part(date_part) != 0) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + + /* Check time part + timezone */ + const char* time_str = t + 1; + const char* plus = strpbrk(time_str, "+-Zz"); + if (!plus) { + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + } + + size_t time_only_len = (size_t)(plus - time_str); + if (time_only_len == 0 || time_only_len >= 32) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + + char time_part[33] = {0}; + memcpy(time_part, time_str, time_only_len); + + int h, m, s; + if (parse_time_part(time_part, &h, &m, &s) != 0) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + + int off_h = 0, off_m = 0; + if (parse_timezone(plus, &off_h, &off_m) != 0) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + /* Reject trailing garbage after timezone */ + if (*plus == 'Z' || *plus == 'z') { + if (plus[1] != '\0') return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + } else { + if (plus[6] != '\0') return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + } + + if (check_leap_second(h, m, s, off_h, off_m) != 0) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + + return CELIX_JANSSON_SCHEMA_OK; +} + +int celix_jansson_check_date(const char* value) { + /* YYYY-MM-DD */ + if (strlen(value) < 10) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + return check_date_part(value) == 0 ? CELIX_JANSSON_SCHEMA_OK : CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; +} + +int celix_jansson_check_time(const char* value) { + /* HH:MM:SS[.fraction](Z|±HH:MM) */ + const char* plus = strpbrk(value, "+-Zz"); + size_t time_len = plus ? (size_t)(plus - value) : strlen(value); + + if (time_len == 0 || time_len > 32) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + + char time_part[33] = {0}; + memcpy(time_part, value, time_len); + + int h, m, s; + if (parse_time_part(time_part, &h, &m, &s) != 0) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + + int off_h = 0, off_m = 0; + if (plus) { + if (parse_timezone(plus, &off_h, &off_m) != 0) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + /* Reject trailing garbage after timezone (e.g., "Z+00:30") */ + if (*plus == 'Z' || *plus == 'z') { + if (plus[1] != '\0') + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + } else { + /* ±HH:MM — 6 chars total */ + if (plus[6] != '\0') + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + } + } else { + /* No timezone — invalid per RFC 3339 */ + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + } + + if (check_leap_second(h, m, s, off_h, off_m) != 0) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + + return CELIX_JANSSON_SCHEMA_OK; +} + +/* ── Email ─────────────────────────────────────────────────────────────── */ + +static bool is_ascii(const char* s) { + for (const unsigned char* p = (const unsigned char*)s; *p; p++) { + if (*p > 127) + return false; + } + return true; +} + +int celix_jansson_check_email(const char* value) { + if (!is_ascii(value)) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + if (!celix_jansson_smtp_is_address(value, value + strlen(value))) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + return CELIX_JANSSON_SCHEMA_OK; +} + +int celix_jansson_check_idn_email(const char* value) { + if (!celix_jansson_smtp_is_address(value, value + strlen(value))) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + return CELIX_JANSSON_SCHEMA_OK; +} + +/* ── Hostname ──────────────────────────────────────────────────────────── */ + +int celix_jansson_check_hostname(const char* value) { + if (!value || *value == '\0') + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + + const char* label_start = value; + size_t total_len = strlen(value); + + if (total_len > 253) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + + while (*label_start) { + /* Find end of label */ + const char* dot = strchr(label_start, '.'); + if (!dot) + dot = label_start + strlen(label_start); + size_t label_len = (size_t)(dot - label_start); + + if (label_len == 0 || label_len > 63) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + + /* First char must be alphanumeric, last must be alphanumeric */ + if (!isalnum((unsigned char)label_start[0])) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + if (!isalnum((unsigned char)dot[-1])) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + + /* Middle chars: alphanumeric or hyphen */ + for (const char* p = label_start + 1; p < dot - 1; p++) { + if (!isalnum((unsigned char)*p) && *p != '-') + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + } + + if (*dot == '\0') + break; + label_start = dot + 1; + } + + return CELIX_JANSSON_SCHEMA_OK; +} + +/* ── IPv4 ──────────────────────────────────────────────────────────────── */ + +int celix_jansson_check_ipv4(const char* value) { + unsigned int a, b, c, d; + char tail; + if (sscanf(value, "%3u.%3u.%3u.%3u%c", &a, &b, &c, &d, &tail) != 4) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + if (a > 255 || b > 255 || c > 255 || d > 255) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + /* Reject leading zeros (e.g., "192.168.01.001") */ + char octet[16]; + /* a,b,c,d <= 255 was validated above, so the formatted string always fits in octet */ + int n = snprintf(octet, sizeof(octet), "%u.%u.%u.%u", a, b, c, d); + assert(n < (int)sizeof(octet)); + (void)n; + if (strcmp(value, octet) != 0) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + return CELIX_JANSSON_SCHEMA_OK; +} + +/* ── IPv6 ──────────────────────────────────────────────────────────────── */ + +int celix_jansson_check_ipv6(const char* value) { + struct in6_addr addr; + /* Reject zone-id forms (%eth0 etc.) — checked before inet_pton, + * which never accepts '%', so this makes the rejection explicit + * and deterministic across platforms */ + if (strchr(value, '%')) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + if (inet_pton(AF_INET6, value, &addr) != 1) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + return CELIX_JANSSON_SCHEMA_OK; +} + +/* ── URI (absolute) ────────────────────────────────────────────────────── */ + +/* RFC 3986 §2.3: unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" */ +static bool uri_is_unreserved(int c) { + return isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~'; +} + +/* RFC 3986 §2.2: sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" */ +static bool uri_is_sub_delims(int c) { + return c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' || + c == ')' || c == '*' || c == '+' || c == ',' || c == ';' || c == '='; +} + +/* Validate percent-encoded octet: % HEXDIG HEXDIG. Returns 3 (bytes + * consumed) on success, -1 on failure. */ +static int uri_check_pct_encoded(const char* p) { + if (p[0] != '%' || !isxdigit((unsigned char)p[1]) || !isxdigit((unsigned char)p[2])) + return -1; + return 3; +} + +/* Validate a path/query/fragment segment character-by-character. + * @p extra_chars additional allowed characters beyond pchar (e.g., "/?" for query). + * Returns 0 if all characters up to @p stop_chars are valid, -1 on invalid char. */ +static int uri_validate_segment(const char** pp, const char* stop_chars, const char* extra_chars) { + const char* p = *pp; + while (*p && !strchr(stop_chars, *p)) { + if (*p == '%') { + int n = uri_check_pct_encoded(p); + if (n < 0) return -1; + p += n; continue; + } + if (uri_is_unreserved(*p) || uri_is_sub_delims(*p) || + *p == ':' || *p == '@' || *p == '/') + { p++; continue; } + /* Check extra chars (e.g., '?' for query/fragment) */ + if (extra_chars && strchr(extra_chars, *p)) + { p++; continue; } + return -1; + } + *pp = p; + return 0; +} + +int celix_jansson_check_uri(const char* value) { + /* Hand-rolled RFC 3986 absolute URI parser */ + if (!value || *value == '\0') + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + + const char* p = value; + + /* Scheme: ALPHA *(ALPHA / DIGIT / "+" / "-" / ".") */ + if (!isalpha((unsigned char)*p)) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + while (isalnum((unsigned char)*p) || *p == '+' || *p == '-' || *p == '.') + p++; + + /* Must have ':' after scheme */ + if (p[0] != ':') + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + p++; /* skip ':' */ + + /* hier-part: "//" authority path-abempty / path-* */ + bool has_authority = (p[0] == '/' && p[1] == '/'); + const char* auth_end = p; + + if (has_authority) { + p += 2; /* skip "//" */ + auth_end = strpbrk(p, "/?#"); + if (!auth_end) + auth_end = p + strlen(p); + + /* Basic authority validation */ + if (auth_end > p) { + /* Host part is present */ + const char* at = memchr(p, '@', (size_t)(auth_end - p)); + if (at) { + /* Skip userinfo */ + p = at + 1; + } + + size_t host_len = (size_t)(auth_end - p); + /* Allow IPv6 literal [...] */ + if (*p == '[') { + const char* bracket = memchr(p, ']', host_len); + if (!bracket) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + /* Validate inner IPv6 */ + size_t ip6_len = (size_t)(bracket - p - 1); + if (ip6_len > 0) { + /* Basic check — skip full validation */ + } + p = bracket + 1; + /* Optional port */ + if (p < auth_end && *p == ':') + p = auth_end; + } else { + /* reg-name or IPv4: validate characters */ + const char* colon = memchr(p, ':', host_len); + const char* host_part_end = colon ? colon : auth_end; + for (const char* c = p; c < host_part_end; c++) { + if (*c == '%') { + int n = uri_check_pct_encoded(c); + if (n < 0) return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + c += 2; continue; + } + if (uri_is_unreserved(*c) || uri_is_sub_delims(*c)) + continue; + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + } + p = host_part_end; + if (colon) { + /* Port must be digits */ + for (const char* c = colon + 1; c < auth_end; c++) { + if (!isdigit((unsigned char)*c)) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + } + p = auth_end; + } + } + } + p = auth_end; + } + + /* Path (optional): segment *( "/" segment ) + * For authority-based URIs this starts with '/'. + * For scheme-only URIs (mailto:, tel:, news:) the rest is a path-rootless. */ + if (*p == '/' || (!has_authority && *p != '?' && *p != '#' && *p != '\0')) { + if (uri_validate_segment(&p, "?#", NULL) != 0) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + } + + /* Query (optional): ? *( pchar / "/" / "?" ) */ + if (*p == '?') { + p++; + if (uri_validate_segment(&p, "#", "?") != 0) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + } + + /* Fragment (optional): # *( pchar / "/" / "?" ) */ + if (*p == '#') { + p++; + if (uri_validate_segment(&p, "", "?") != 0) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + } + + return CELIX_JANSSON_SCHEMA_OK; +} + +/* ── UUID ──────────────────────────────────────────────────────────────── */ + +int celix_jansson_check_uuid(const char* value) { + /* 8-4-4-4-12 hex pattern: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx */ + if (strlen(value) != 36) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + + for (int i = 0; i < 36; i++) { + if (i == 8 || i == 13 || i == 18 || i == 23) { + if (value[i] != '-') + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + } else { + if (!isxdigit((unsigned char)value[i])) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + } + } + return CELIX_JANSSON_SCHEMA_OK; +} + +/* ── Regex ─────────────────────────────────────────────────────────────── */ + +int celix_jansson_check_regex(const char* value) { + regex_t re; + int rc = regcomp(&re, value, REG_EXTENDED | REG_NOSUB); + if (rc != 0) { + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + } + regfree(&re); + return CELIX_JANSSON_SCHEMA_OK; +} + +/* ── Main dispatch ─────────────────────────────────────────────────────── */ + +int celix_jansson_schema_default_format_check(const char* format, const char* value, void* user_data) { + (void)user_data; + if (!format || !value) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + + if (strcmp(format, "date-time") == 0) + return celix_jansson_check_date_time(value); + if (strcmp(format, "date") == 0) + return celix_jansson_check_date(value); + if (strcmp(format, "time") == 0) + return celix_jansson_check_time(value); + if (strcmp(format, "email") == 0) + return celix_jansson_check_email(value); + if (strcmp(format, "idn-email") == 0) + return celix_jansson_check_idn_email(value); + if (strcmp(format, "hostname") == 0) + return celix_jansson_check_hostname(value); + if (strcmp(format, "ipv4") == 0) + return celix_jansson_check_ipv4(value); + if (strcmp(format, "ipv6") == 0) + return celix_jansson_check_ipv6(value); + if (strcmp(format, "uri") == 0) + return celix_jansson_check_uri(value); + if (strcmp(format, "uuid") == 0) + return celix_jansson_check_uuid(value); + if (strcmp(format, "regex") == 0) + return celix_jansson_check_regex(value); + + /* Known but unsupported draft-7 formats */ + static const char* unsupported[] = {"idn-hostname", + "uri-reference", + "iri", + "iri-reference", + "uri-template", + "json-pointer", + "relative-json-pointer", + NULL}; + for (const char** u = unsupported; *u; u++) { + if (strcmp(format, *u) == 0) + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; + } + + /* Unknown format */ + return CELIX_JANSSON_SCHEMA_ERROR_INVALID_ARGUMENT; +} diff --git a/libs/jansson_ext/src/celix_string_format_check.h b/libs/jansson_ext/src/celix_string_format_check.h new file mode 100644 index 000000000..9d078c798 --- /dev/null +++ b/libs/jansson_ext/src/celix_string_format_check.h @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#ifndef CELIX_CELIX_STRING_FORMAT_CHECK_H +#define CELIX_CELIX_STRING_FORMAT_CHECK_H + +#include "celix_jansson_schema.h" + +/** RFC 3339 date-time validator. Returns CELIX_JANSSON_SCHEMA_OK or error code. */ +int celix_jansson_check_date_time(const char* value); + +/** RFC 3339 date validator. */ +int celix_jansson_check_date(const char* value); + +/** RFC 3339 time validator. */ +int celix_jansson_check_time(const char* value); + +/** RFC 5321 email address validator. Returns CELIX_JANSSON_SCHEMA_OK or error code. */ +int celix_jansson_check_email(const char* value); + +/** RFC 6531 international email address validator (no ASCII restriction). */ +int celix_jansson_check_idn_email(const char* value); + +/** Hostname (DNS label) validator per RFC 3986 Appendix A. */ +int celix_jansson_check_hostname(const char* value); + +/** IPv4 address validator. */ +int celix_jansson_check_ipv4(const char* value); + +/** IPv6 address validator. */ +int celix_jansson_check_ipv6(const char* value); + +/** Absolute URI validator per RFC 3986. */ +int celix_jansson_check_uri(const char* value); + +/** UUID validator per RFC 4122 (8-4-4-4-12 hex pattern). */ +int celix_jansson_check_uuid(const char* value); + +/** ECMAScript regex validator — attempts to compile the string as a regex. */ +int celix_jansson_check_regex(const char* value); + +#endif /* CELIX_CELIX_STRING_FORMAT_CHECK_H */ diff --git a/libs/jansson_ext/src/celix_util.c b/libs/jansson_ext/src/celix_util.c new file mode 100644 index 000000000..abe9328eb --- /dev/null +++ b/libs/jansson_ext/src/celix_util.c @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#include "celix_util.h" +#include + +/* ── String buffer ────────────────────────────────────────────────────── */ + +int celix_jansson_strbuf_append(celix_jansson_strbuf_t* sb, const char* s, size_t len) { + if (len == 0) + return 0; + size_t needed = sb->len + len + 1; /* +1 for NUL */ + if (needed > sb->cap) { + size_t new_cap = sb->cap ? sb->cap * 2 : 64; + while (new_cap < needed) + new_cap *= 2; + char* new_data = (char*)realloc(sb->data, new_cap); + if (!new_data) + return -1; + sb->data = new_data; + sb->cap = new_cap; + } + memcpy(sb->data + sb->len, s, len); + sb->len += len; + sb->data[sb->len] = '\0'; + return 0; +} + +int celix_jansson_strbuf_appendf(celix_jansson_strbuf_t* sb, const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + int r = celix_jansson_strbuf_vappendf(sb, fmt, ap); + va_end(ap); + return r; +} + +int celix_jansson_strbuf_vappendf(celix_jansson_strbuf_t* sb, const char* fmt, va_list ap) { + va_list ap2; + va_copy(ap2, ap); + int needed = vsnprintf(NULL, 0, fmt, ap2); + va_end(ap2); + if (needed < 0) + return -1; + + size_t total_needed = sb->len + (size_t)needed + 1; + if (total_needed > sb->cap) { + size_t new_cap = sb->cap ? sb->cap * 2 : 64; + while (new_cap < total_needed) + new_cap *= 2; + char* new_data = (char*)realloc(sb->data, new_cap); + if (!new_data) + return -1; + sb->data = new_data; + sb->cap = new_cap; + } + + va_copy(ap2, ap); + vsnprintf(sb->data + sb->len, sb->cap - sb->len, fmt, ap2); + va_end(ap2); + sb->len += (size_t)needed; + return 0; +} + +char* celix_jansson_strbuf_detach(celix_jansson_strbuf_t* sb) { + if (sb->len == 0) + return NULL; + char* s = sb->data; + sb->data = NULL; + sb->len = 0; + sb->cap = 0; + return s; +} + +/* ── Dynamic pointer array ──────────────────────────────────────────────── */ + +int celix_jansson_vec_push(celix_jansson_vec_t* v, void* item) { + if (v->len >= v->cap) { + size_t new_cap = v->cap ? v->cap * 2 : 4; + void** new_items = (void**)realloc(v->items, new_cap * sizeof(void*)); + if (!new_items) + return -1; + v->items = new_items; + v->cap = new_cap; + } + v->items[v->len++] = item; + return 0; +} + +void* celix_jansson_vec_pop(celix_jansson_vec_t* v) { + if (v->len == 0) + return NULL; + return v->items[--v->len]; +} diff --git a/libs/jansson_ext/src/celix_util.h b/libs/jansson_ext/src/celix_util.h new file mode 100644 index 000000000..dcdb900a0 --- /dev/null +++ b/libs/jansson_ext/src/celix_util.h @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +#ifndef CELIX_CELIX_UTIL_H +#define CELIX_CELIX_UTIL_H + +#include "celix_cleanup.h" +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ── Dynamic string buffer ─────────────────────────────────────────────── */ + +typedef struct celix_jansson_strbuf_t { + char* data; + size_t len; + size_t cap; +} celix_jansson_strbuf_t; + +static inline void celix_jansson_strbuf_init(celix_jansson_strbuf_t* sb) { + sb->data = NULL; + sb->len = 0; + sb->cap = 0; +} + +static inline void celix_jansson_strbuf_free(celix_jansson_strbuf_t* sb) { + free(sb->data); + sb->data = NULL; + sb->len = 0; + sb->cap = 0; +} + +/* Enables `celix_auto(celix_jansson_strbuf_t) sb;` for scope-based automatic + * cleanup. Safe on uninitialized (zeroed) buffers and after detach. */ +CELIX_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(celix_jansson_strbuf_t, celix_jansson_strbuf_free) + +/** Append @p len bytes from @p s. Returns 0 on success, -1 on ENOMEM. */ +int celix_jansson_strbuf_append(celix_jansson_strbuf_t* sb, const char* s, size_t len); + +/** Append a printf-formatted string. Returns 0 on success, -1 on ENOMEM. */ +int celix_jansson_strbuf_appendf(celix_jansson_strbuf_t* sb, const char* fmt, ...) __attribute__((format(printf, 2, 3))); + +/** Va_list variant of celix_jansson_strbuf_appendf. */ +int celix_jansson_strbuf_vappendf(celix_jansson_strbuf_t* sb, const char* fmt, va_list ap) __attribute__((format(printf,2,0))); + +/** Append a single character. */ +static inline int celix_jansson_strbuf_appendc(celix_jansson_strbuf_t* sb, char c) { + return celix_jansson_strbuf_append(sb, &c, 1); +} + +/** Append a NUL-terminated string. */ +static inline int celix_jansson_strbuf_appends(celix_jansson_strbuf_t* sb, const char* s) { + return celix_jansson_strbuf_append(sb, s, strlen(s)); +} + +/** + * Detach the accumulated string. Returns a malloc'd, NUL-terminated C + * string; the strbuf is re-initialized to empty. Returns NULL if empty. + */ +char* celix_jansson_strbuf_detach(celix_jansson_strbuf_t* sb); + +/* ── Dynamic pointer array ──────────────────────────────────────────────── */ + +typedef struct celix_jansson_vec_t { + void** items; + size_t len; + size_t cap; +} celix_jansson_vec_t; + +static inline void celix_jansson_vec_init(celix_jansson_vec_t* v) { + v->items = NULL; + v->len = 0; + v->cap = 0; +} + +static inline void celix_jansson_vec_free(celix_jansson_vec_t* v) { + free(v->items); + v->items = NULL; + v->len = 0; + v->cap = 0; +} + +int celix_jansson_vec_push(celix_jansson_vec_t* v, void* item); +void* celix_jansson_vec_pop(celix_jansson_vec_t* v); + +static inline void* celix_jansson_vec_get(const celix_jansson_vec_t* v, size_t i) { + return (i < v->len) ? v->items[i] : NULL; +} + +static inline size_t celix_jansson_vec_size(const celix_jansson_vec_t* v) { return v->len; } + +#ifdef __cplusplus +} +#endif + +#endif /* CELIX_CELIX_UTIL_H */ diff --git a/rat-excludes.txt b/rat-excludes.txt index a17bae1ec..c5ede9aac 100644 --- a/rat-excludes.txt +++ b/rat-excludes.txt @@ -60,6 +60,8 @@ scope.*\.json #MIT - C Thread Pool thpool.c thpool.h +#Third-party JSON Schema Test Suite data files (no license headers) +JSON-Schema-Test-Suite Design.md FAQ.md README.md diff --git a/test_package/CMakeLists.txt b/test_package/CMakeLists.txt index 5ba1b25e2..34d660c06 100644 --- a/test_package/CMakeLists.txt +++ b/test_package/CMakeLists.txt @@ -298,6 +298,12 @@ if (TEST_CELIX_DFI) target_link_libraries(use_celix_dfi PRIVATE Celix::dfi) endif () +option(TEST_JANSSON_EXT "Test jansson_ext" OFF) +if (TEST_JANSSON_EXT) + add_executable(use_jansson_ext test_jansson_ext.c) + target_link_libraries(use_jansson_ext PRIVATE Celix::jansson_ext) +endif () + option(TEST_UTILS "Test utils" OFF) if (TEST_UTILS) add_executable(use_utils test_utils.c) diff --git a/test_package/conanfile.py b/test_package/conanfile.py index 57a2ddc97..64299f9b6 100644 --- a/test_package/conanfile.py +++ b/test_package/conanfile.py @@ -63,6 +63,7 @@ def generate(self): tc.cache_variables["TEST_CXX_REMOTE_SERVICE_ADMIN"] = celix_options.build_cxx_remote_service_admin tc.cache_variables["TEST_SHELL_API"] = celix_options.build_shell_api tc.cache_variables["TEST_CELIX_DFI"] = celix_options.build_celix_dfi + tc.cache_variables["TEST_JANSSON_EXT"] = celix_options.build_jansson_ext tc.cache_variables["TEST_UTILS"] = celix_options.build_utils tc.cache_variables["TEST_EVENT_ADMIN"] = celix_options.build_event_admin tc.cache_variables["TEST_EVENT_ADMIN_REMOTE_PROVIDER_MQTT"] = celix_options.build_event_admin_remote_provider_mqtt @@ -138,6 +139,8 @@ def test(self): self.run("./use_shell_api", env="conanrun") if celix_options.build_celix_dfi: self.run("./use_celix_dfi", env="conanrun") + if celix_options.build_jansson_ext: + self.run("./use_jansson_ext", env="conanrun") if celix_options.build_utils: self.run("./use_utils", env="conanrun") if celix_options.build_event_admin: diff --git a/test_package/test_jansson_ext.c b/test_package/test_jansson_ext.c new file mode 100644 index 000000000..96f0fcf8e --- /dev/null +++ b/test_package/test_jansson_ext.c @@ -0,0 +1,238 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +#include +#include +#include +#include +#include +#include +#include +#include + +/* Applies a merge patch and checks the result against the expected JSON. */ +static int applyAndExpectEqual(const char* targetText, const char* patchText, const char* expectedText) { + json_error_t jerr; + json_t* target = json_loads(targetText, JSON_DECODE_ANY, &jerr); + json_t* patch = json_loads(patchText, JSON_DECODE_ANY, &jerr); + json_t* expected = json_loads(expectedText, JSON_DECODE_ANY, &jerr); + if (!target || !patch || !expected) { + fprintf(stderr, "Error parsing merge patch test input: %s\n", jerr.text); + json_decref(target); + json_decref(patch); + json_decref(expected); + return 1; + } + + json_t* result = celix_json_merge_patch(target, patch); + int rc = result && json_equal(result, expected) ? 0 : 1; + if (rc != 0) { + char* resultStr = json_dumps(result, JSON_ENCODE_ANY); + fprintf(stderr, + "Merge patch mismatch:\n target = %s\n patch = %s\n expected = %s\n got = %s\n", + targetText, patchText, expectedText, resultStr ? resultStr : "?"); + free(resultStr); + } + json_decref(result); + json_decref(target); + json_decref(patch); + json_decref(expected); + return rc; +} + +int main() { + /* ── JSON Schema draft-7 validation ─────────────────────────────────── */ + json_error_t jerr; + json_t* schema = json_loads( + "{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}},\"required\":[\"name\"]}", + 0, &jerr); + if (!schema) { + fprintf(stderr, "Error parsing schema: %s\n", jerr.text); + return 1; + } + + celix_jansson_schema_validator_t* validator = celix_jansson_schema_validator_create( + NULL, NULL, celix_jansson_schema_default_format_check, NULL, NULL, NULL); + if (!validator) { + fprintf(stderr, "Failed to create validator\n"); + json_decref(schema); + return 1; + } + + char* errmsg = NULL; + int rc = celix_jansson_schema_set_root_schema(validator, schema, &errmsg); + if (rc != CELIX_JANSSON_SCHEMA_OK) { + fprintf(stderr, "Schema compilation error: %s\n", errmsg ? errmsg : celix_jansson_schema_strerror(rc)); + free(errmsg); + json_decref(schema); + celix_jansson_schema_validator_destroy(validator); + return 1; + } + free(errmsg); + json_decref(schema); + + json_t* instance = json_loads("{\"name\":\"celix\"}", 0, &jerr); + if (!instance) { + fprintf(stderr, "Error parsing instance: %s\n", jerr.text); + celix_jansson_schema_validator_destroy(validator); + return 1; + } + int errors = celix_jansson_schema_validate(validator, instance, NULL, NULL, NULL); + printf("valid instance errors = %d\n", errors); + json_decref(instance); + if (errors != 0) { + celix_jansson_schema_validator_destroy(validator); + return 1; + } + + instance = json_loads("{}", 0, &jerr); + if (!instance) { + fprintf(stderr, "Error parsing instance: %s\n", jerr.text); + celix_jansson_schema_validator_destroy(validator); + return 1; + } + errors = celix_jansson_schema_validate(validator, instance, NULL, NULL, NULL); + printf("invalid instance errors = %d\n", errors); + json_decref(instance); + celix_jansson_schema_validator_destroy(validator); + if (errors == 0) { + fprintf(stderr, "Expected invalid instance to be rejected\n"); + return 1; + } + + /* ── JSON Pointer (RFC 6901) ───────────────────────────────────────── */ + json_t* doc = json_loads("{\"foo\":{\"bar\":1}}", 0, &jerr); + if (!doc) { + fprintf(stderr, "Error parsing document: %s\n", jerr.text); + return 1; + } + celix_json_pointer_t* ptr = celix_json_pointer_create("/foo/bar"); + json_t* value = ptr ? celix_json_pointer_get(doc, ptr) : NULL; + char* ptr_str = ptr ? celix_json_pointer_to_string(ptr) : NULL; + if (!ptr || !value || !ptr_str || strcmp(ptr_str, "/foo/bar") != 0) { + fprintf(stderr, "JSON pointer resolution failed\n"); + free(ptr_str); + celix_json_pointer_destroy(ptr); + json_decref(doc); + return 1; + } + printf("pointer = %s, value = %lld\n", ptr_str, json_integer_value(value)); + free(ptr_str); + + if (celix_json_pointer_set_new(doc, ptr, json_integer(42)) != 0) { + fprintf(stderr, "JSON pointer set failed\n"); + celix_json_pointer_destroy(ptr); + json_decref(doc); + return 1; + } + printf("value after set = %lld\n", json_integer_value(celix_json_pointer_get(doc, ptr))); + celix_json_pointer_destroy(ptr); + + /* ── JSON Patch (RFC 6902) ─────────────────────────────────────────── */ + json_t* patch = json_array(); + if (!patch || celix_json_patch_add(patch, "/foo/bar", json_integer(7)) != 0) { + fprintf(stderr, "JSON patch add failed\n"); + json_decref(patch); + json_decref(doc); + return 1; + } + json_t* patched = celix_json_patch_apply(doc, patch); + if (!patched || json_integer_value(json_object_get(json_object_get(patched, "foo"), "bar")) != 7) { + fprintf(stderr, "JSON patch apply failed\n"); + json_decref(patched); + json_decref(patch); + json_decref(doc); + return 1; + } + char* patched_str = json_dumps(patched, 0); + printf("patched = %s\n", patched_str ? patched_str : "?"); + free(patched_str); + json_decref(patched); + json_decref(patch); + json_decref(doc); + + /* ── JSON Merge Patch (RFC 7396) ────────────────────────────────────── */ + /* Section 1 example and Appendix A examples, as in the gtest suite */ + int mergePatchFailures = 0; + mergePatchFailures += applyAndExpectEqual("{\"a\":\"b\",\"c\":{\"d\":\"e\",\"f\":\"g\"}}", + "{\"a\":\"z\",\"c\":{\"f\":null}}", + "{\"a\":\"z\",\"c\":{\"d\":\"e\"}}"); + mergePatchFailures += applyAndExpectEqual("{\"a\":\"b\"}", "{\"b\":\"c\"}", "{\"a\":\"b\",\"b\":\"c\"}"); /* add member */ + mergePatchFailures += applyAndExpectEqual("{\"a\":\"b\"}", "{\"a\":null}", "{}"); /* remove member */ + mergePatchFailures += applyAndExpectEqual("[\"a\",\"b\"]", "[\"c\",\"d\"]", "[\"c\",\"d\"]"); /* non-object patch */ + mergePatchFailures += applyAndExpectEqual("{\"a\":\"foo\"}", "null", "null"); /* null patch */ + mergePatchFailures += applyAndExpectEqual("{\"a\":\"foo\"}", "\"bar\"", "\"bar\""); /* scalar patch */ + mergePatchFailures += applyAndExpectEqual("[1,2]", "{\"a\":\"b\",\"c\":null}", "{\"a\":\"b\"}"); /* object patch on array */ + mergePatchFailures += applyAndExpectEqual("{\"a\":1}", "{}", "{\"a\":1}"); /* empty patch on object */ + mergePatchFailures += applyAndExpectEqual("[1,2]", "{}", "{}"); /* empty patch on non-object */ + mergePatchFailures += applyAndExpectEqual("{}", "{\"a\":{\"bb\":{\"ccc\":null}}}", "{\"a\":{\"bb\":{}}}"); /* nested absent member */ + if (mergePatchFailures != 0) { + fprintf(stderr, "JSON merge patch tests failed\n"); + return 1; + } + printf("merge patch RFC 7396 examples ok\n"); + + /* NULL arguments are rejected */ + doc = json_loads("{\"a\":1}", 0, &jerr); + if (!doc) { + fprintf(stderr, "Error parsing merge patch document: %s\n", jerr.text); + return 1; + } + if (celix_json_merge_patch(NULL, doc) != NULL || celix_json_merge_patch(doc, NULL) != NULL || + celix_json_merge_patch(NULL, NULL) != NULL) { + fprintf(stderr, "Merge patch with NULL argument(s) should return NULL\n"); + json_decref(doc); + return 1; + } + json_decref(doc); + + /* inputs are never modified and a new document is returned */ + doc = json_loads("{\"a\":{\"b\":1},\"c\":[1,2]}", 0, &jerr); + patch = json_loads("{\"a\":{\"b\":2}}", 0, &jerr); + if (!doc || !patch) { + fprintf(stderr, "Error parsing merge patch inputs: %s\n", jerr.text); + json_decref(doc); + json_decref(patch); + return 1; + } + json_t* docCopy = json_deep_copy(doc); + json_t* patchCopy = json_deep_copy(patch); + json_t* merged = celix_json_merge_patch(doc, patch); + if (!docCopy || !patchCopy || !merged || merged == doc || merged == patch || + !json_equal(doc, docCopy) || !json_equal(patch, patchCopy) || + json_integer_value(json_object_get(json_object_get(merged, "a"), "b")) != 2 || + !json_object_get(merged, "c")) { + fprintf(stderr, "Merge patch should return a new document without modifying its inputs\n"); + json_decref(merged); + json_decref(patchCopy); + json_decref(docCopy); + json_decref(patch); + json_decref(doc); + return 1; + } + char* merged_str = json_dumps(merged, 0); + printf("merged = %s\n", merged_str ? merged_str : "?"); + free(merged_str); + json_decref(merged); + json_decref(patchCopy); + json_decref(docCopy); + json_decref(patch); + json_decref(doc); + + return 0; +}