From 64e947f832c028d6d82227fa4d0e9f733145cf47 Mon Sep 17 00:00:00 2001 From: Clemens Vasters Date: Tue, 14 Jul 2026 09:46:31 +0200 Subject: [PATCH 1/2] Make R SDK CRAN-conformant via vendored source build Refactor the R package to compile the JSON Structure C engine from vendored sources at install time instead of downloading a prebuilt shared library at runtime. CRAN forbids install/runtime network access, so the download model could never be accepted; this makes the package self-contained and installable from source on every platform. Engine sourcing - Vendor the engine translation units (error_codes.c, instance_validator.c, json_source_locator.c, schema_validator.c, types.c), the public headers under src/json_structure/, and cJSON v1.7.18 (MIT, (c) Dave Gamble) into r/src/. cJSON attribution is recorded in inst/COPYRIGHTS. - Add tools/vendor-engine.R to re-sync the engine sources/headers from c/ into r/src/ (kept out of the build tarball via .Rbuildignore). - Remove the runtime binary installer (R/binary_installer.R and its docs); drop the download/dlopen path from ffi.R/zzz.R. Windows std::regex deadlock -> pure-C matcher - MinGW libstdc++ std::regex deadlocks on first use whenever its DLL is loaded by R on Windows (lazy locale init via __gthread_once under the fully-static Rtools toolchain). This is unfixable by link flags, so the vendored regex_utils.cpp is replaced with regex_utils.c, a self-contained ECMAScript-subset backtracking matcher (pure C, no threads/TLS, ReDoS step budget). The package is now pure C: no libstdc++ dependency. - The upstream C SDK (c/src/regex_utils.cpp) is unchanged; only the R vendored copy differs. regex_utils.h documents the split. C engine (shared source of truth) - Replace three bounded snprintf copies in instance_validator.c and schema_validator.c with memcpy to silence -Wformat-truncation so the vendored build is warning-free after any re-sync. C SDK tests unaffected. Build, CI, docs - Makevars: Unix keeps $(SHLIB_PTHREAD_FLAGS) for the engine allocator lock; Makevars.win links nothing extra (SRWLOCK from kernel32). No C++/libstdc++. - .github/workflows/r.yml: single source-build matrix over ubuntu/macos/windows, R CMD check with --as-cran and error-on warning, corpus exercised via JSONSTRUCTURE_TEST_ASSETS; drop the separate C-build and Windows jobs. - Update README/NEWS/cran-comments and regenerate man/ for the pure-C, source-build model. R CMD check --as-cran: 0 errors, 0 warnings, 2 notes (new submission; cJSON pragma), with the shared test-assets corpus passing during the check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/r.yml | 60 +- c/src/instance_validator.c | 10 +- c/src/schema_validator.c | 10 +- r/.Rbuildignore | 1 + r/DESCRIPTION | 25 +- r/NAMESPACE | 9 +- r/NEWS.md | 44 +- r/R/binary_installer.R | 307 -- r/R/ffi.R | 64 +- r/R/instance_validator.R | 3 +- r/R/jsonstructure-package.R | 14 +- r/R/schema_validator.R | 3 +- r/R/version.R | 33 +- r/R/zzz.R | 14 +- r/README.md | 81 +- r/cran-comments.md | 31 + r/inst/COPYRIGHTS | 22 + r/man/install_jsonstructure_binary.Rd | 50 - r/man/js_validate_instance.Rd | 2 - r/man/js_validate_schema.Rd | 2 - r/man/jsonstructure-package.Rd | 12 +- r/man/jsonstructure_binary_path.Rd | 21 - r/man/jsonstructure_binary_version.Rd | 25 - r/man/jsonstructure_engine_version.Rd | 19 + r/man/jsonstructure_version.Rd | 2 +- r/src/Makevars | 26 +- r/src/Makevars.win | 13 +- r/src/cJSON.c | 3143 +++++++++++++++++++++ r/src/cjson/cJSON.h | 300 ++ r/src/error_codes.c | 212 ++ r/src/init.c | 24 +- r/src/instance_validator.c | 1990 +++++++++++++ r/src/json_source_locator.c | 438 +++ r/src/json_structure/error_codes.h | 267 ++ r/src/json_structure/export.h | 51 + r/src/json_structure/instance_validator.h | 117 + r/src/json_structure/json.h | 334 +++ r/src/json_structure/json_structure.h | 124 + r/src/json_structure/schema_validator.h | 115 + r/src/json_structure/types.h | 513 ++++ r/src/regex_utils.c | 980 +++++++ r/src/regex_utils.h | 51 + r/src/schema_validator.c | 2029 +++++++++++++ r/src/shim.c | 302 +- r/src/types.c | 655 +++++ r/tests/testthat/helper-jsonstructure.R | 31 +- r/tools/vendor-engine.R | 116 + 47 files changed, 11723 insertions(+), 972 deletions(-) delete mode 100644 r/R/binary_installer.R create mode 100644 r/cran-comments.md create mode 100644 r/inst/COPYRIGHTS delete mode 100644 r/man/install_jsonstructure_binary.Rd delete mode 100644 r/man/jsonstructure_binary_path.Rd delete mode 100644 r/man/jsonstructure_binary_version.Rd create mode 100644 r/man/jsonstructure_engine_version.Rd create mode 100644 r/src/cJSON.c create mode 100644 r/src/cjson/cJSON.h create mode 100644 r/src/error_codes.c create mode 100644 r/src/instance_validator.c create mode 100644 r/src/json_source_locator.c create mode 100644 r/src/json_structure/error_codes.h create mode 100644 r/src/json_structure/export.h create mode 100644 r/src/json_structure/instance_validator.h create mode 100644 r/src/json_structure/json.h create mode 100644 r/src/json_structure/json_structure.h create mode 100644 r/src/json_structure/schema_validator.h create mode 100644 r/src/json_structure/types.h create mode 100644 r/src/regex_utils.c create mode 100644 r/src/regex_utils.h create mode 100644 r/src/schema_validator.c create mode 100644 r/src/types.c create mode 100644 r/tools/vendor-engine.R diff --git a/.github/workflows/r.yml b/.github/workflows/r.yml index dbbfadf..4cd99c7 100644 --- a/.github/workflows/r.yml +++ b/.github/workflows/r.yml @@ -27,26 +27,20 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest, macos-latest, windows-latest] r: ['release', 'oldrel-1'] env: R_KEEP_PKG_SOURCE: yes + # The engine is compiled from bundled source, so the check builds and + # links the native code on every OS. Point the shared conformance corpus + # at the repo checkout so the test-assets tests (including `pattern` + # cases) run inside R CMD check on all platforms instead of skipping. + JSONSTRUCTURE_TEST_ASSETS: ${{ github.workspace }}/test-assets steps: - uses: actions/checkout@v6 - - name: Build C library (CI only - production uses pre-built release binaries) - shell: bash - run: | - cmake -S c -B c/build -DCMAKE_BUILD_TYPE=Release -DJS_BUILD_SHARED=ON -DJS_BUILD_TESTS=OFF -DJS_BUILD_EXAMPLES=OFF - cmake --build c/build --config Release - if [ "$RUNNER_OS" = "macOS" ]; then - echo "JSONSTRUCTURE_LIB_PATH=$GITHUB_WORKSPACE/c/build/libjson_structure.dylib" >> "$GITHUB_ENV" - else - echo "JSONSTRUCTURE_LIB_PATH=$GITHUB_WORKSPACE/c/build/libjson_structure.so" >> "$GITHUB_ENV" - fi - - uses: r-lib/actions/setup-r@v2 with: r-version: ${{ matrix.r }} @@ -62,46 +56,10 @@ jobs: - uses: r-lib/actions/check-r-package@v2 with: working-directory: r - error-on: '"error"' + args: 'c("--no-manual", "--as-cran")' + error-on: '"warning"' upload-snapshots: true - - name: Conformance tests against shared test-assets - working-directory: r - shell: bash - env: - JSONSTRUCTURE_TEST_ASSETS: ${{ github.workspace }}/test-assets - run: | - R CMD INSTALL . - Rscript -e "library(testthat); library(jsonstructure); testthat::test_dir('tests/testthat', stop_on_failure = TRUE)" - - windows: - name: Check R release on windows-latest (shim compile + pure-R tests) - runs-on: windows-latest - env: - R_KEEP_PKG_SOURCE: yes - steps: - - uses: actions/checkout@v6 - - - uses: r-lib/actions/setup-r@v2 - with: - r-version: 'release' - use-public-rspm: true - - - uses: r-lib/actions/setup-r-dependencies@v2 - with: - working-directory: r - extra-packages: | - any::rcmdcheck - any::testthat - - # No prebuilt library is provided here, so binding-dependent tests skip - # gracefully. This job verifies the compiled shim builds under Rtools and - # that the pure-R logic (augmentation, result objects) passes on Windows. - - uses: r-lib/actions/check-r-package@v2 - with: - working-directory: r - error-on: '"error"' - lint: name: Lint R code runs-on: ubuntu-latest @@ -129,7 +87,7 @@ jobs: release: name: Attach R source tarball to release runs-on: ubuntu-latest - needs: [test, windows, lint] + needs: [test, lint] if: startsWith(github.ref, 'refs/tags/v') permissions: contents: write diff --git a/c/src/instance_validator.c b/c/src/instance_validator.c index cec672b..d10597d 100644 --- a/c/src/instance_validator.c +++ b/c/src/instance_validator.c @@ -65,12 +65,16 @@ static void push_path(validate_context_t* ctx, const char* segment) { return; /* Path too long, skip appending */ } + /* Buffer space was validated above, so the segment is guaranteed to + * fit; copy directly to avoid -Wformat-truncation false positives that + * arise from snprintf's runtime bound. */ if (len == 0) { - snprintf(ctx->path, PATH_BUFFER_SIZE, "%s", segment); + memcpy(ctx->path, segment, seg_len + 1); } else if (segment[0] == '[') { - snprintf(ctx->path + len, PATH_BUFFER_SIZE - len, "%s", segment); + memcpy(ctx->path + len, segment, seg_len + 1); } else { - snprintf(ctx->path + len, PATH_BUFFER_SIZE - len, ".%s", segment); + ctx->path[len] = '.'; + memcpy(ctx->path + len + 1, segment, seg_len + 1); } } diff --git a/c/src/schema_validator.c b/c/src/schema_validator.c index 4c3c1c7..2844aeb 100644 --- a/c/src/schema_validator.c +++ b/c/src/schema_validator.c @@ -102,12 +102,16 @@ static void push_path(validate_context_t* ctx, const char* segment) { return; /* Path too long, skip appending */ } + /* Buffer space was validated above, so the segment is guaranteed to + * fit; copy directly to avoid -Wformat-truncation false positives that + * arise from snprintf's runtime bound. */ if (len == 0) { - snprintf(ctx->path, PATH_BUFFER_SIZE, "%s", segment); + memcpy(ctx->path, segment, seg_len + 1); } else if (segment[0] == '[') { - snprintf(ctx->path + len, PATH_BUFFER_SIZE - len, "%s", segment); + memcpy(ctx->path + len, segment, seg_len + 1); } else { - snprintf(ctx->path + len, PATH_BUFFER_SIZE - len, ".%s", segment); + ctx->path[len] = '.'; + memcpy(ctx->path + len + 1, segment, seg_len + 1); } } diff --git a/r/.Rbuildignore b/r/.Rbuildignore index 269bd06..48d409f 100644 --- a/r/.Rbuildignore +++ b/r/.Rbuildignore @@ -6,6 +6,7 @@ ^\.gitignore$ ^\.gitattributes$ ^cran-comments\.md$ +^tools$ ^doc$ ^Meta$ ^src/.*\.(o|so|dll|dylib|a)$ diff --git a/r/DESCRIPTION b/r/DESCRIPTION index c5cb626..166a317 100644 --- a/r/DESCRIPTION +++ b/r/DESCRIPTION @@ -4,17 +4,16 @@ Title: JSON Structure Schema and Instance Validation Version: 0.1.0 Authors@R: c( person("Clemens", "Vasters", email = "clemensv@microsoft.com", role = c("aut", "cre")), - person("JSON Structure Contributors", role = "cph")) + person("JSON Structure Contributors", role = "cph"), + person("Dave", "Gamble", role = c("ctb", "cph"), + comment = "Author of the bundled cJSON library (src/cJSON.c, src/cjson/cJSON.h)")) Description: R bindings for the JSON Structure validator, providing schema validation and instance validation against JSON Structure schemas. The - package binds to the JSON Structure C library ('json_structure') for the - performance-critical validation work: a small compiled shim loads a - prebuilt shared library. That library is obtained on demand via - install_jsonstructure_binary() (downloaded from GitHub Releases with an - integrity check) or supplied through the JSONSTRUCTURE_LIB_PATH environment - variable; it is never downloaded without the user's action or consent. - Additional extension-keyword checks (units, relations, alternate names and - identifier rules) are performed in R. + performance-critical validation is carried out by the JSON Structure C + engine, which is compiled from bundled source directly into the package + together with its 'cJSON' dependency; nothing is downloaded at install or + run time. Additional extension-keyword checks (units, relations, alternate + names and identifier rules) are performed in R. License: MIT + file LICENSE URL: https://github.com/json-structure/sdk, https://json-structure.org BugReports: https://github.com/json-structure/sdk/issues @@ -22,17 +21,9 @@ Encoding: UTF-8 Depends: R (>= 4.0) Imports: jsonlite, - tools, utils Suggests: - digest, - openssl, testthat (>= 3.0.0) -SystemRequirements: GNU make. A C compiler is required to build the package. - The prebuilt 'json_structure' shared library is obtained separately, either - by calling install_jsonstructure_binary() (which downloads it from GitHub - Releases; network access required) or by setting the JSONSTRUCTURE_LIB_PATH - environment variable to a local build. Config/testthat/edition: 3 Roxygen: list(markdown = TRUE) Config/roxygen2/version: 8.0.0 diff --git a/r/NAMESPACE b/r/NAMESPACE index 54183ff..28ad9ca 100644 --- a/r/NAMESPACE +++ b/r/NAMESPACE @@ -6,7 +6,6 @@ S3method(is_valid,default) S3method(is_valid,js_validation_result) S3method(print,js_validation_error) S3method(print,js_validation_result) -export(install_jsonstructure_binary) export(is_valid) export(js_error_messages) export(js_validate_instance) @@ -14,15 +13,9 @@ export(js_validate_instance_strict) export(js_validate_schema) export(js_validate_schema_strict) export(js_warning_messages) -export(jsonstructure_binary_path) -export(jsonstructure_binary_version) +export(jsonstructure_engine_version) export(jsonstructure_version) importFrom(jsonlite,fromJSON) importFrom(jsonlite,toJSON) -importFrom(tools,R_user_dir) -importFrom(utils,askYesNo) -importFrom(utils,download.file) importFrom(utils,packageVersion) -importFrom(utils,untar) -importFrom(utils,unzip) useDynLib(jsonstructure, .registration = TRUE, .fixes = "C_") diff --git a/r/NEWS.md b/r/NEWS.md index 3120af9..b39cfdd 100644 --- a/r/NEWS.md +++ b/r/NEWS.md @@ -1,14 +1,38 @@ # jsonstructure 0.1.0 -* Initial release of the JSON Structure R SDK. -* Schema validation via `js_validate_schema()`. -* Instance validation via `js_validate_instance()`. -* Bindings to the JSON Structure C library through a small compiled shim that - loads a prebuilt `json_structure` shared library. The library is never - downloaded implicitly: point `JSONSTRUCTURE_LIB_PATH` at a local build, or run - `install_jsonstructure_binary()` (or consent to the one-time interactive - prompt) to fetch it from GitHub Releases. Downloads are checksum-verified when - an expected SHA-256 is supplied via `sha256=`, `JSONSTRUCTURE_BINARY_SHA256`, - or a shipped `checksums.dcf` manifest. +## Native engine compiled from bundled source (CRAN-conformant) + +The JSON Structure C validation engine and its `cJSON` dependency are now +**bundled in the package and compiled from source at install time**. Nothing is +downloaded at install or run time, so the package installs from source on any +platform with a C toolchain and is conformant with CRAN policy. The package is +pure C, with no C++ runtime dependency. + +This replaces the earlier prototype, which loaded a prebuilt `json_structure` +shared library downloaded from GitHub Releases on first use. + +### Breaking changes + +* Removed `install_jsonstructure_binary()` and `jsonstructure_binary_path()` — + there is no longer a separate native library to download or locate. +* Removed the `JSONSTRUCTURE_LIB_PATH` environment-variable override. +* Renamed `jsonstructure_binary_version()` to `jsonstructure_engine_version()`, + which reports the version of the bundled engine. + +### Features + +* Schema validation via `js_validate_schema()` / `js_validate_schema_strict()`. +* Instance validation via `js_validate_instance()` / + `js_validate_instance_strict()`. +* Tidy S3 results with `is_valid()`, `js_error_messages()`, + `js_warning_messages()` and `as.data.frame()`. * Additional extension-keyword checks (units, relations, alternate identifier rules, `$extends`, enum value typing) performed in R, matching the Ruby SDK. + +### Notes + +* `pattern` constraints are matched by a small embedded ECMAScript-subset + regular-expression engine (pure C), giving identical behaviour on every + platform with no C++/`std::regex` dependency. Constructs outside the subset + (lookbehind, named groups, inline flags, Unicode property escapes) are treated + as invalid patterns, and backtracking is bounded against pathological inputs. diff --git a/r/R/binary_installer.R b/r/R/binary_installer.R deleted file mode 100644 index 6d35cfc..0000000 --- a/r/R/binary_installer.R +++ /dev/null @@ -1,307 +0,0 @@ -# Downloads and caches the prebuilt json_structure shared library from GitHub -# Releases. Mirrors ruby/lib/jsonstructure/binary_installer.rb. -# -# The package ships only a thin compiled shim; the actual validation engine is -# the prebuilt C library. It is NEVER downloaded implicitly during validation: -# the download happens only when the user explicitly calls -# install_jsonstructure_binary(), or, in an interactive session, agrees to the -# consent prompt raised on first use. Set JSONSTRUCTURE_LIB_PATH to point at a -# locally built library to skip the download entirely. - -`%||%` <- function(a, b) if (is.null(a) || length(a) == 0 || identical(a, "")) b else a - -# Normalise a CPU architecture string to the tokens used in release assets. -.js_normalize_arch <- function(arch = R.version$arch) { - arch <- tolower(arch) - if (grepl("aarch64|arm64", arch)) { - "arm64" - } else if (grepl("x86_64|x86-64|amd64|x64", arch)) { - "x86_64" - } else { - arch - } -} - -# Operating system token used in release asset names. -.js_os_token <- function(sysname = Sys.info()[["sysname"]]) { - switch(sysname, - Windows = "windows", - Darwin = "macos", - Linux = "linux", - tolower(sysname) - ) -} - -# Platform token, e.g. "linux-x86_64", "macos-arm64", "windows-x86_64". -.js_platform <- function() { - paste0(.js_os_token(), "-", .js_normalize_arch()) -} - -# File name of the shared library for this platform. -.js_binary_name <- function(sysname = Sys.info()[["sysname"]]) { - switch(sysname, - Windows = "json_structure.dll", - Darwin = "libjson_structure.dylib", - "libjson_structure.so" - ) -} - -# Regex matching the shared-library files to copy out of the archive. -.js_lib_pattern <- function(sysname = Sys.info()[["sysname"]]) { - switch(sysname, - Windows = "\\.dll$", - Darwin = "\\.dylib$", - "\\.so$" - ) -} - -# Per-user cache directory holding the downloaded library. -.js_lib_dir <- function(create = FALSE) { - dir <- tools::R_user_dir("jsonstructure", which = "cache") - if (create && !dir.exists(dir)) { - dir.create(dir, recursive = TRUE, showWarnings = FALSE) - } - dir -} - -.js_cached_binary_path <- function() { - file.path(.js_lib_dir(create = FALSE), .js_binary_name()) -} - -# URL of the release asset for this platform. Overridable for testing / mirrors. -.js_download_url <- function(version = JSONSTRUCTURE_BINARY_VERSION, - platform = .js_platform()) { - full <- Sys.getenv("JSONSTRUCTURE_BINARY_URL", unset = "") - if (nzchar(full)) { - return(full) - } - base <- Sys.getenv( - "JSONSTRUCTURE_BINARY_BASEURL", - unset = sprintf("https://github.com/%s/releases/download", JSONSTRUCTURE_REPO) - ) - sprintf("%s/%s/json_structure-%s.tar.gz", base, version, platform) -} - -#' Path to the resolved json_structure library, if available -#' -#' Returns the path to the shared library that would be (or has been) loaded: -#' the `JSONSTRUCTURE_LIB_PATH` override if set, otherwise the cached download. -#' -#' @return A file path, or `NULL` if no library is available locally. -#' @seealso [install_jsonstructure_binary()] -#' @examples -#' jsonstructure_binary_path() -#' @export -jsonstructure_binary_path <- function() { - override <- Sys.getenv("JSONSTRUCTURE_LIB_PATH", unset = "") - if (nzchar(override) && file.exists(override)) { - return(override) - } - cached <- .js_cached_binary_path() - if (file.exists(cached)) cached else NULL -} - -#' Download and cache the prebuilt json_structure library -#' -#' Downloads the prebuilt shared library for the current platform from the -#' project's GitHub Releases and caches it under -#' \code{tools::R_user_dir("jsonstructure", "cache")}. The library is never -#' downloaded implicitly by the validators; call this function explicitly to -#' install or update it (in an interactive session, the validators will offer -#' to call it on first use). Set the \code{JSONSTRUCTURE_LIB_PATH} environment -#' variable to point at a locally built library and skip downloading entirely. -#' -#' @param version Release tag to download (defaults to the pinned binary -#' version). -#' @param force Re-download even if a cached copy exists. -#' @param quiet Suppress download progress and integrity messages. -#' @param sha256 Optional expected SHA-256 (hex) of the downloaded archive. When -#' supplied (or when \code{JSONSTRUCTURE_BINARY_SHA256} is set, or a shipped -#' checksum manifest matches), the download is verified before extraction and -#' an error is raised on mismatch. When no expected digest is available the -#' computed digest is reported so it can be pinned. -#' @return (Invisibly) the path to the cached shared library. -#' @seealso [jsonstructure_binary_path()], [jsonstructure_binary_version()] -#' @examples -#' \dontrun{ -#' # Downloads the prebuilt engine for the current platform (network access): -#' install_jsonstructure_binary() -#' # Enforce integrity against a known digest: -#' install_jsonstructure_binary(sha256 = "e3b0c4...") -#' } -#' @export -install_jsonstructure_binary <- function(version = NULL, force = FALSE, - quiet = FALSE, sha256 = NULL) { - version <- version %||% JSONSTRUCTURE_BINARY_VERSION - dir <- .js_lib_dir(create = TRUE) - target <- .js_cached_binary_path() - - if (file.exists(target) && !force) { - return(invisible(target)) - } - - url <- .js_download_url(version = version) - tmp <- tempfile(fileext = if (grepl("\\.zip$", url)) ".zip" else ".tar.gz") - on.exit(unlink(tmp, force = TRUE), add = TRUE) - - ok <- tryCatch({ - utils::download.file(url, destfile = tmp, mode = "wb", quiet = quiet) - TRUE - }, error = function(e) { - stop(sprintf( - "Failed to download the json_structure library from '%s': %s\nSet JSONSTRUCTURE_LIB_PATH to a locally built library to bypass the download.", - url, conditionMessage(e)), call. = FALSE) - }) - if (!isTRUE(ok) || !file.exists(tmp)) { - stop(sprintf("Failed to download the json_structure library from '%s'.", url), - call. = FALSE) - } - - # Verify integrity before we extract and later load native code. - .js_verify_download(tmp, url = url, version = version, - platform = .js_platform(), sha256 = sha256, quiet = quiet) - - exdir <- file.path(dir, "extract") - unlink(exdir, recursive = TRUE, force = TRUE) - dir.create(exdir, recursive = TRUE, showWarnings = FALSE) - on.exit(unlink(exdir, recursive = TRUE, force = TRUE), add = TRUE) - - if (grepl("\\.zip$", url)) { - utils::unzip(tmp, exdir = exdir) - } else { - utils::untar(tmp, exdir = exdir) - } - - # The archive layout is not guaranteed to be flat (CMake install prefixes - # place the library under lib/ or bin/), so search recursively. Prefer the - # file whose name exactly matches this platform's expected library name and - # only fall back to a pattern match, so unexpected archive contents cannot - # silently substitute a differently named library. - expected_name <- .js_binary_name() - all_libs <- list.files(exdir, pattern = .js_lib_pattern(), recursive = TRUE, - full.names = TRUE) - libs <- all_libs[basename(all_libs) == expected_name] - if (length(libs) == 0) { - libs <- all_libs - } - for (f in libs) { - file.copy(f, file.path(dir, basename(f)), overwrite = TRUE) - } - - if (!file.exists(target)) { - stop(sprintf( - "Downloaded archive from '%s' did not contain the expected library '%s'.", - url, expected_name), call. = FALSE) - } - - invisible(target) -} - -# --- Integrity verification ------------------------------------------------ - -# Compute the SHA-256 (lowercase hex) of a file, using whatever hashing -# facility is available: base tools (R >= 4.5), then the openssl or digest -# packages. Returns NA_character_ if none is available. -.js_sha256 <- function(file) { - tools_ns <- asNamespace("tools") - if (exists("sha256sum", where = tools_ns, inherits = FALSE)) { - return(tolower(unname(get("sha256sum", tools_ns)(file)))) - } - if (requireNamespace("openssl", quietly = TRUE)) { - con <- file(file, "rb") - on.exit(close(con), add = TRUE) - # paste(collapse=) strips the openssl "hash" class and guarantees a plain, - # single lowercase hex string regardless of the openssl version in use. - return(tolower(paste(as.character(openssl::sha256(con)), collapse = ""))) - } - if (requireNamespace("digest", quietly = TRUE)) { - return(tolower(digest::digest(file, algo = "sha256", file = TRUE))) - } - NA_character_ -} - -# Expected checksum shipped with the package, if any. Looks up a DCF manifest -# (inst/checksums.dcf) keyed by Version + Platform. Returns "" when no manifest -# or matching entry exists. -.js_manifest_sha256 <- function(version, platform) { - path <- system.file("checksums.dcf", package = "jsonstructure") - if (!nzchar(path) || !file.exists(path)) { - return("") - } - recs <- tryCatch(read.dcf(path), error = function(e) NULL) - if (is.null(recs) || !all(c("Version", "Platform", "SHA256") %in% colnames(recs))) { - return("") - } - hit <- recs[, "Version"] == version & recs[, "Platform"] == platform - if (any(hit)) tolower(recs[which(hit)[1], "SHA256"]) else "" -} - -# Verify a downloaded archive against an expected SHA-256. Resolution order for -# the expected digest: explicit `sha256` arg -> JSONSTRUCTURE_BINARY_SHA256 -> -# shipped manifest. Aborts on mismatch; warns (but proceeds) when no digest is -# available to verify against. -.js_verify_download <- function(file, url, version, platform, sha256 = NULL, - quiet = FALSE) { - expected <- sha256 %||% Sys.getenv("JSONSTRUCTURE_BINARY_SHA256", unset = "") - if (!nzchar(expected)) { - expected <- .js_manifest_sha256(version, platform) - } - actual <- .js_sha256(file) - - if (is.na(actual)) { - warning("Could not compute a SHA-256 of the downloaded archive (no hashing ", - "facility available); integrity was not verified. Install the ", - "'openssl' package to enable verification.", call. = FALSE) - return(invisible(NA_character_)) - } - - if (nzchar(expected)) { - if (!isTRUE(tolower(expected) == actual)) { - stop(sprintf( - "Checksum mismatch for '%s':\n expected %s\n actual %s\nThe download may be corrupt or tampered with; aborting.", - url, tolower(expected), actual), call. = FALSE) - } - if (!quiet) { - message(sprintf("Verified SHA-256 of %s.", basename(url))) - } - } else if (!quiet) { - message(sprintf( - "Downloaded %s\n SHA-256: %s\n No expected checksum was available to verify against; pass sha256= or set JSONSTRUCTURE_BINARY_SHA256 to enforce integrity.", - basename(url), actual)) - } - invisible(actual) -} - -# Resolve the library path with explicit user consent. Called only when no -# library is available via JSONSTRUCTURE_LIB_PATH or the cache. In an -# interactive session the user is asked before any download; otherwise a clear, -# actionable error is raised (no implicit network access). -.js_prompt_and_install <- function() { - detail <- paste0( - "The json_structure native library is not installed. It can be downloaded ", - "from GitHub Releases into the per-user cache (", - .js_lib_dir(create = FALSE), "), or you can set the JSONSTRUCTURE_LIB_PATH ", - "environment variable to a locally built library.") - - if (!interactive()) { - stop(detail, - "\nRun install_jsonstructure_binary() to download it, or set ", - "JSONSTRUCTURE_LIB_PATH.", call. = FALSE) - } - - consent <- utils::askYesNo( - paste0(detail, "\nDownload it now?"), default = FALSE) - if (!isTRUE(consent)) { - stop("json_structure library not available and download was declined. ", - "Run install_jsonstructure_binary() or set JSONSTRUCTURE_LIB_PATH.", - call. = FALSE) - } - - install_jsonstructure_binary() - path <- jsonstructure_binary_path() - if (is.null(path)) { - stop("Installation did not produce a usable json_structure library.", - call. = FALSE) - } - path -} diff --git a/r/R/ffi.R b/r/R/ffi.R index acac083..5728cd9 100644 --- a/r/R/ffi.R +++ b/r/R/ffi.R @@ -1,28 +1,9 @@ # Low-level wrappers over the compiled shim (.Call entry points). # -# The C shim (src/shim.c) loads the prebuilt json_structure shared library at -# runtime via dlopen/LoadLibraryEx. These helpers wrap the registered .Call -# routines. The `C_*` symbols are created by useDynLib(..., .fixes = "C_") in -# NAMESPACE. - -# Whether the prebuilt json_structure library is currently loaded by the shim. -js_binding_loaded <- function() { - isTRUE(.Call(C_binding_loaded)) -} - -# Load the prebuilt library from `path`; stop() with a helpful message on error. -.js_load_binding <- function(path) { - err <- .Call(C_load_library, path) - if (nzchar(err)) { - stop(sprintf("Failed to load the json_structure library from '%s': %s", - path, err), call. = FALSE) - } - invisible(TRUE) -} - -.js_unload_binding <- function() { - invisible(.Call(C_unload_library)) -} +# The JSON Structure C engine is compiled directly into this package's shared +# object (see src/), so there is no runtime library to load, download or +# resolve: these helpers simply forward to the registered native routines. The +# `C_*` symbols are created by useDynLib(..., .fixes = "C_") in NAMESPACE. .js_call_validate_schema <- function(schema_json) { .Call(C_validate_schema, schema_json) @@ -32,37 +13,8 @@ js_binding_loaded <- function() { .Call(C_validate_instance, instance_json, schema_json) } -# Ensure the prebuilt library is loaded, resolving it lazily on first use from -# the JSONSTRUCTURE_LIB_PATH override or the per-user cache. The native engine -# is never downloaded implicitly: if no library is available the user is asked -# for consent (interactive sessions only), otherwise a clear error explains how -# to install it. This keeps validation offline-safe and non-interactive / CRAN -# runs quiet and free of surprise network access. -.js_ensure_loaded <- function() { - if (js_binding_loaded()) { - return(invisible(TRUE)) - } - - override <- Sys.getenv("JSONSTRUCTURE_LIB_PATH", unset = "") - if (nzchar(override) && !file.exists(override)) { - stop(sprintf( - "JSONSTRUCTURE_LIB_PATH is set to '%s' but that file does not exist.", - override), call. = FALSE) - } - - path <- jsonstructure_binary_path() - if (is.null(path)) { - path <- .js_prompt_and_install() - } - - .js_load_binding(path) -} - -# Version string reported by the loaded json_structure engine, or "" when the -# library is not loaded or does not export runtime version information. -js_binding_version <- function() { - if (!js_binding_loaded()) { - return("") - } - .Call(C_binding_version) +# Version string reported by the JSON Structure C engine compiled into this +# package. +.js_engine_version <- function() { + .Call(C_engine_version) } diff --git a/r/R/instance_validator.R b/r/R/instance_validator.R index cd224c7..cf30c12 100644 --- a/r/R/instance_validator.R +++ b/r/R/instance_validator.R @@ -10,14 +10,13 @@ #' Structure schema to validate against. #' @return A `js_validation_result` object. #' @seealso [js_validate_schema()], [js_validate_instance_strict()] -#' @examplesIf !is.null(jsonstructure::jsonstructure_binary_path()) +#' @examples #' res <- js_validate_instance('"hello"', '{"type":"string"}') #' is_valid(res) #' @export js_validate_instance <- function(instance, schema) { instance_json <- .js_as_json_text(instance, "instance") schema_json <- .js_as_json_text(schema, "schema") - .js_ensure_loaded() res <- .js_call_validate_instance(instance_json, schema_json) .js_result_from_call(res) } diff --git a/r/R/jsonstructure-package.R b/r/R/jsonstructure-package.R index 1447871..c3509aa 100644 --- a/r/R/jsonstructure-package.R +++ b/r/R/jsonstructure-package.R @@ -1,22 +1,20 @@ #' jsonstructure: JSON Structure validation for R #' #' Validate JSON Structure schemas and JSON instances against them. The package -#' is a thin binding over the proven JSON Structure C engine: a small compiled -#' shim loads a prebuilt `json_structure` shared library (installed on demand -#' with [install_jsonstructure_binary()], or supplied via the -#' `JSONSTRUCTURE_LIB_PATH` environment variable) and marshals results back into -#' idiomatic R objects. +#' is a thin binding over the JSON Structure C engine, which is compiled from +#' bundled source directly into the package: validation runs in native code and +#' the results are marshalled back into idiomatic R objects. Nothing is +#' downloaded at install or run time. #' #' @section Main functions: #' \itemize{ #' \item [js_validate_schema()] / [js_validate_schema_strict()] #' \item [js_validate_instance()] / [js_validate_instance_strict()] -#' \item [install_jsonstructure_binary()], [jsonstructure_binary_path()] +#' \item [jsonstructure_version()], [jsonstructure_engine_version()] #' } #' #' @useDynLib jsonstructure, .registration = TRUE, .fixes = "C_" #' @importFrom jsonlite fromJSON toJSON -#' @importFrom tools R_user_dir -#' @importFrom utils askYesNo download.file untar unzip packageVersion +#' @importFrom utils packageVersion #' @keywords internal "_PACKAGE" diff --git a/r/R/schema_validator.R b/r/R/schema_validator.R index f8a7b50..2d690eb 100644 --- a/r/R/schema_validator.R +++ b/r/R/schema_validator.R @@ -47,13 +47,12 @@ #' outcome and [js_error_messages()] / [js_warning_messages()] to inspect #' diagnostics. #' @seealso [js_validate_instance()], [js_validate_schema_strict()] -#' @examplesIf !is.null(jsonstructure::jsonstructure_binary_path()) +#' @examples #' res <- js_validate_schema('{"type":"string"}') #' is_valid(res) #' @export js_validate_schema <- function(schema) { schema_json <- .js_as_json_text(schema, "schema") - .js_ensure_loaded() res <- .js_call_validate_schema(schema_json) base <- .js_result_from_call(res) .js_augment_schema_result(base, schema_json) diff --git a/r/R/version.R b/r/R/version.R index 2285672..b199194 100644 --- a/r/R/version.R +++ b/r/R/version.R @@ -1,16 +1,9 @@ # Version metadata for the JSON Structure R SDK. -# The prebuilt C library release tag this package is pinned to. Mirrors the -# Ruby SDK's BinaryInstaller::DEFAULT_VERSION. -JSONSTRUCTURE_BINARY_VERSION <- "v0.1.0" - -# GitHub repository that hosts the prebuilt binaries. -JSONSTRUCTURE_REPO <- "json-structure/sdk" - #' Package version #' #' @return The installed package version as a character string. -#' @seealso [jsonstructure_binary_version()] for the native engine version. +#' @seealso [jsonstructure_engine_version()] for the bundled C engine version. #' @examples #' jsonstructure_version() #' @export @@ -18,24 +11,16 @@ jsonstructure_version <- function() { as.character(utils::packageVersion("jsonstructure")) } -#' Version of the loaded json_structure native engine +#' Version of the bundled JSON Structure C engine #' -#' Reports the version string exposed by the prebuilt `json_structure` shared -#' library currently loaded by the shim. Older engine builds do not export a -#' runtime version symbol; in that case (or when no engine is loaded) the -#' release tag this package is pinned to is returned instead. +#' Reports the version string of the JSON Structure C engine that is compiled +#' into this package. The engine and its cJSON dependency are built from source +#' at install time; nothing is downloaded at run time. #' -#' @return A single character string: the engine's reported version, or the -#' pinned target version (without the leading `v`) when the engine does not -#' report one. -#' @seealso [install_jsonstructure_binary()], [jsonstructure_binary_path()] +#' @return A single character string: the bundled engine's version. #' @examples -#' jsonstructure_binary_version() +#' jsonstructure_engine_version() #' @export -jsonstructure_binary_version <- function() { - reported <- tryCatch(js_binding_version(), error = function(e) "") - if (is.character(reported) && length(reported) == 1L && nzchar(reported)) { - return(reported) - } - sub("^v", "", JSONSTRUCTURE_BINARY_VERSION) +jsonstructure_engine_version <- function() { + .js_engine_version() } diff --git a/r/R/zzz.R b/r/R/zzz.R index cff3bd7..aac2661 100644 --- a/r/R/zzz.R +++ b/r/R/zzz.R @@ -1,12 +1,12 @@ -# Package load/unload hooks. +# Package unload hook. # -# Loading of the prebuilt json_structure engine is deferred entirely to the -# first validation call (see .js_ensure_loaded()): we do NOT load external -# native code during library()/.onLoad, which keeps package attach fast, -# side-effect-free and offline-safe. .onUnload releases the engine and the -# compiled shim when the package is unloaded. +# The JSON Structure C engine is compiled into the package shared object and is +# initialised in R_init_jsonstructure() when that object loads, so there is no +# .onLoad work to do. On unload we ask the engine to release its process-wide +# resources (the compiled-regex cache) before detaching the shared object, so a +# subsequent reload starts from a clean state. .onUnload <- function(libpath) { - try(.js_unload_binding(), silent = TRUE) + try(.Call(C_engine_cleanup), silent = TRUE) library.dynam.unload("jsonstructure", libpath) } diff --git a/r/README.md b/r/README.md index 3236068..e0062bd 100644 --- a/r/README.md +++ b/r/README.md @@ -1,9 +1,10 @@ # JSON Structure R SDK R package for [JSON Structure](https://json-structure.org) schema and instance -validation. It binds to the JSON Structure **C engine** through a small compiled -shim: the heavy validation work runs in native code, while the R layer provides -an idiomatic API and a few extra extension-keyword checks. +validation. It bundles the JSON Structure **C engine** and compiles it from +source directly into the package: the heavy validation work runs in native code, +while the R layer provides an idiomatic API and a few extra extension-keyword +checks. Nothing is downloaded at install or run time. ## Features @@ -14,37 +15,35 @@ an idiomatic API and a few extra extension-keyword checks. objects with `is_valid()`, `js_error_messages()` and `as.data.frame()`. - **Cross-platform** — Linux, macOS and Windows. -## How the native library is provided +## How the native engine is provided -The package ships only a thin compiled shim. On first use it downloads a -prebuilt `json_structure` shared library for your platform from the project's -GitHub Releases and caches it under `tools::R_user_dir("jsonstructure", "cache")` -— mirroring the Ruby SDK. No C toolchain is needed at *runtime* (a C compiler is -needed once to build the shim when the package is installed). +The JSON Structure validation engine (portable C99) and its `cJSON` dependency +are **bundled in the package** and compiled from source when the package is +installed. Nothing is fetched from the network at install or run time, so +installs are reproducible and offline-friendly. -Supported platforms: Linux (x86_64, arm64), macOS (x86_64, arm64), -Windows (x86_64). +Building from source requires a C toolchain: -To use a locally built library and skip the download, set the -`JSONSTRUCTURE_LIB_PATH` environment variable to the full path of the shared -library (e.g. `c/build/libjson_structure.so`). +- **Linux / macOS** — the system C compiler (as for any package with + compiled code). +- **Windows** — [Rtools](https://cran.r-project.org/bin/windows/Rtools/) + matching your R version. + +The package is pure C (no C++ runtime dependency). `pattern` constraints are +handled by a small embedded regular-expression matcher, so behaviour is +identical on every platform. +Supported platforms: Linux, macOS and Windows (x86_64 and arm64). ## Installation -The package is not on CRAN (the download-at-first-use model is incompatible with -CRAN policy). Install from GitHub: +Because the engine is compiled from bundled source, the package is +CRAN-policy-conformant. Install from GitHub: ```r # install.packages("remotes") remotes::install_github("json-structure/sdk", subdir = "r") ``` -You can also pre-fetch the native library: - -```r -jsonstructure::install_jsonstructure_binary() -``` - ## Usage ### Schema validation @@ -103,25 +102,17 @@ tryCatch( | `is_valid(result)` | `TRUE`/`FALSE` | | `js_error_messages(result)` / `js_warning_messages(result)` | Messages by severity | | `as.data.frame(result)` | Errors as a data frame | -| `install_jsonstructure_binary()` | Pre-fetch the native library | -| `jsonstructure_binary_path()` | Path to the resolved native library | | `jsonstructure_version()` | Installed package version | +| `jsonstructure_engine_version()` | Version of the bundled native engine | ## Development -Build the C library once and point the package at it: - -```bash -# from the repo root -cmake -S c -B c/build -DJS_BUILD_SHARED=ON -cmake --build c/build -export JSONSTRUCTURE_LIB_PATH="$PWD/c/build/libjson_structure.so" # .dylib / .dll -``` - -Then, from the `r/` directory: +The native engine sources are vendored under `src/` and compiled together with +the R bindings, so a normal `devtools` workflow builds everything. From the `r/` +directory: ```r -devtools::load_all() +devtools::load_all() # compiles the bundled engine + shim devtools::test() # runs testthat, including the shared test-assets corpus ``` @@ -129,19 +120,25 @@ devtools::test() # runs testthat, including the shared test-assets corp ```bash R CMD build r -R CMD check jsonstructure_*.tar.gz +R CMD check --as-cran jsonstructure_*.tar.gz ``` -Tests that need the native library skip automatically when it is not available, -and the shared `test-assets` conformance tests skip when the corpus is not -present (e.g. when checking the built tarball outside the repo). +The shared `test-assets` conformance tests skip automatically when the corpus is +not present (e.g. when checking the built tarball outside the repo). + +The vendored engine sources are kept in sync with `c/` by +`tools/vendor-engine.R` (see that script's header for how to refresh them). ## Limitations - No schema **exporter** in v1 (the C engine has none), matching the Ruby SDK. -- Regex/pattern constraints are only enforced if the prebuilt C library was - built with regex support; the default build ships with it disabled, the same - behaviour as the C and Ruby SDKs. +- `pattern` constraints are matched by an embedded ECMAScript-subset regular + expression engine. It covers the constructs used by JSON Structure schemas + (literals, `.`, anchors, character classes and shorthands, `\b`, greedy/lazy + quantifiers, groups and alternation). Constructs outside that subset — + lookbehind, named groups, inline flags such as `(?i)`, and Unicode property + escapes `\p{...}` — are treated as invalid patterns, and backtracking is + bounded to avoid pathological "ReDoS" inputs. ## License diff --git a/r/cran-comments.md b/r/cran-comments.md new file mode 100644 index 0000000..3e0dfed --- /dev/null +++ b/r/cran-comments.md @@ -0,0 +1,31 @@ +## R CMD check results + +0 errors | 0 warnings | 2 notes + +* This is a new submission. + +* checking pragmas in C/C++ headers and code ... NOTE + File which contains pragma(s) suppressing diagnostics: 'src/cJSON.c' + + `src/cJSON.c` is the unmodified, vendored cJSON library (v1.7.18, MIT, + © Dave Gamble and cJSON contributors). It contains a + `#pragma GCC diagnostic ignored "-Wcast-qual"` in its upstream source. The + file is bundled verbatim so that the package's native engine has no external + dependency; the pragma only takes effect under `-Wcast-qual`, which is not part + of the default R compilation flags. Authorship and copyright are recorded in + `DESCRIPTION` and `inst/COPYRIGHTS`. + +## Test environments + +* Windows 11 x86_64, R 4.6.1 (local), gcc 14.3.0 (Rtools45) +* GitHub Actions: ubuntu-latest, macos-latest, windows-latest; R release and + oldrel-1 + +## Notes on compiled code + +The package bundles and compiles from source the JSON Structure validation +engine (portable C99) together with cJSON. Nothing is downloaded at install or +run time. The package is pure C and links only with the C runtime; `pattern` +keyword matching is provided by a small embedded regular-expression engine, so +there is no `std::regex`/C++ dependency and behaviour is identical on every +platform. diff --git a/r/inst/COPYRIGHTS b/r/inst/COPYRIGHTS new file mode 100644 index 0000000..8a719c5 --- /dev/null +++ b/r/inst/COPYRIGHTS @@ -0,0 +1,22 @@ +The jsonstructure package bundles third-party source code that is compiled +into the package's shared object. Each component is distributed under the MIT +License, compatible with this package's license. + +------------------------------------------------------------------------------- +JSON Structure C engine + Files: src/types.c, src/error_codes.c, src/schema_validator.c, + src/instance_validator.c, src/json_source_locator.c, + src/regex_utils.cpp, src/regex_utils.h, + src/json_structure/*.h + Copyright: (c) 2024 JSON Structure Contributors + License: MIT (SPDX-License-Identifier: MIT) + Source: https://github.com/json-structure/sdk (directory: c/) + +------------------------------------------------------------------------------- +cJSON + Files: src/cJSON.c, src/cjson/cJSON.h + Version: 1.7.18 + Copyright: (c) 2009-2017 Dave Gamble and cJSON contributors + License: MIT (SPDX-License-Identifier: MIT) + Source: https://github.com/DaveGamble/cJSON (tag v1.7.18) +------------------------------------------------------------------------------- diff --git a/r/man/install_jsonstructure_binary.Rd b/r/man/install_jsonstructure_binary.Rd deleted file mode 100644 index ddf6ee8..0000000 --- a/r/man/install_jsonstructure_binary.Rd +++ /dev/null @@ -1,50 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/binary_installer.R -\name{install_jsonstructure_binary} -\alias{install_jsonstructure_binary} -\title{Download and cache the prebuilt json_structure library} -\usage{ -install_jsonstructure_binary( - version = NULL, - force = FALSE, - quiet = FALSE, - sha256 = NULL -) -} -\arguments{ -\item{version}{Release tag to download (defaults to the pinned binary -version).} - -\item{force}{Re-download even if a cached copy exists.} - -\item{quiet}{Suppress download progress and integrity messages.} - -\item{sha256}{Optional expected SHA-256 (hex) of the downloaded archive. When -supplied (or when \code{JSONSTRUCTURE_BINARY_SHA256} is set, or a shipped -checksum manifest matches), the download is verified before extraction and -an error is raised on mismatch. When no expected digest is available the -computed digest is reported so it can be pinned.} -} -\value{ -(Invisibly) the path to the cached shared library. -} -\description{ -Downloads the prebuilt shared library for the current platform from the -project's GitHub Releases and caches it under -\code{tools::R_user_dir("jsonstructure", "cache")}. The library is never -downloaded implicitly by the validators; call this function explicitly to -install or update it (in an interactive session, the validators will offer -to call it on first use). Set the \code{JSONSTRUCTURE_LIB_PATH} environment -variable to point at a locally built library and skip downloading entirely. -} -\examples{ -\dontrun{ -# Downloads the prebuilt engine for the current platform (network access): -install_jsonstructure_binary() -# Enforce integrity against a known digest: -install_jsonstructure_binary(sha256 = "e3b0c4...") -} -} -\seealso{ -\code{\link[=jsonstructure_binary_path]{jsonstructure_binary_path()}}, \code{\link[=jsonstructure_binary_version]{jsonstructure_binary_version()}} -} diff --git a/r/man/js_validate_instance.Rd b/r/man/js_validate_instance.Rd index 5f238f4..b3cdd49 100644 --- a/r/man/js_validate_instance.Rd +++ b/r/man/js_validate_instance.Rd @@ -20,10 +20,8 @@ A \code{js_validation_result} object. Validate a JSON instance against a JSON Structure schema } \examples{ -\dontshow{if (!is.null(jsonstructure::jsonstructure_binary_path())) withAutoprint(\{ # examplesIf} res <- js_validate_instance('"hello"', '{"type":"string"}') is_valid(res) -\dontshow{\}) # examplesIf} } \seealso{ \code{\link[=js_validate_schema]{js_validate_schema()}}, \code{\link[=js_validate_instance_strict]{js_validate_instance_strict()}} diff --git a/r/man/js_validate_schema.Rd b/r/man/js_validate_schema.Rd index 4b166be..597ca49 100644 --- a/r/man/js_validate_schema.Rd +++ b/r/man/js_validate_schema.Rd @@ -19,10 +19,8 @@ diagnostics. Validates that \code{schema} is a well-formed JSON Structure schema document. } \examples{ -\dontshow{if (!is.null(jsonstructure::jsonstructure_binary_path())) withAutoprint(\{ # examplesIf} res <- js_validate_schema('{"type":"string"}') is_valid(res) -\dontshow{\}) # examplesIf} } \seealso{ \code{\link[=js_validate_instance]{js_validate_instance()}}, \code{\link[=js_validate_schema_strict]{js_validate_schema_strict()}} diff --git a/r/man/jsonstructure-package.Rd b/r/man/jsonstructure-package.Rd index 66db4c2..cc4929a 100644 --- a/r/man/jsonstructure-package.Rd +++ b/r/man/jsonstructure-package.Rd @@ -7,18 +7,17 @@ \title{jsonstructure: JSON Structure validation for R} \description{ Validate JSON Structure schemas and JSON instances against them. The package -is a thin binding over the proven JSON Structure C engine: a small compiled -shim loads a prebuilt \code{json_structure} shared library (installed on demand -with \code{\link[=install_jsonstructure_binary]{install_jsonstructure_binary()}}, or supplied via the -\code{JSONSTRUCTURE_LIB_PATH} environment variable) and marshals results back into -idiomatic R objects. +is a thin binding over the JSON Structure C engine, which is compiled from +bundled source directly into the package: validation runs in native code and +the results are marshalled back into idiomatic R objects. Nothing is +downloaded at install or run time. } \section{Main functions}{ \itemize{ \item \code{\link[=js_validate_schema]{js_validate_schema()}} / \code{\link[=js_validate_schema_strict]{js_validate_schema_strict()}} \item \code{\link[=js_validate_instance]{js_validate_instance()}} / \code{\link[=js_validate_instance_strict]{js_validate_instance_strict()}} -\item \code{\link[=install_jsonstructure_binary]{install_jsonstructure_binary()}}, \code{\link[=jsonstructure_binary_path]{jsonstructure_binary_path()}} +\item \code{\link[=jsonstructure_version]{jsonstructure_version()}}, \code{\link[=jsonstructure_engine_version]{jsonstructure_engine_version()}} } } @@ -42,6 +41,7 @@ Authors: Other contributors: \itemize{ \item JSON Structure Contributors [copyright holder] + \item Dave Gamble (Author of the bundled cJSON library (src/cJSON.c, src/cjson/cJSON.h)) [contributor, copyright holder] } } diff --git a/r/man/jsonstructure_binary_path.Rd b/r/man/jsonstructure_binary_path.Rd deleted file mode 100644 index f8640b4..0000000 --- a/r/man/jsonstructure_binary_path.Rd +++ /dev/null @@ -1,21 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/binary_installer.R -\name{jsonstructure_binary_path} -\alias{jsonstructure_binary_path} -\title{Path to the resolved json_structure library, if available} -\usage{ -jsonstructure_binary_path() -} -\value{ -A file path, or \code{NULL} if no library is available locally. -} -\description{ -Returns the path to the shared library that would be (or has been) loaded: -the \code{JSONSTRUCTURE_LIB_PATH} override if set, otherwise the cached download. -} -\examples{ -jsonstructure_binary_path() -} -\seealso{ -\code{\link[=install_jsonstructure_binary]{install_jsonstructure_binary()}} -} diff --git a/r/man/jsonstructure_binary_version.Rd b/r/man/jsonstructure_binary_version.Rd deleted file mode 100644 index f61e41b..0000000 --- a/r/man/jsonstructure_binary_version.Rd +++ /dev/null @@ -1,25 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/version.R -\name{jsonstructure_binary_version} -\alias{jsonstructure_binary_version} -\title{Version of the loaded json_structure native engine} -\usage{ -jsonstructure_binary_version() -} -\value{ -A single character string: the engine's reported version, or the -pinned target version (without the leading \code{v}) when the engine does not -report one. -} -\description{ -Reports the version string exposed by the prebuilt \code{json_structure} shared -library currently loaded by the shim. Older engine builds do not export a -runtime version symbol; in that case (or when no engine is loaded) the -release tag this package is pinned to is returned instead. -} -\examples{ -jsonstructure_binary_version() -} -\seealso{ -\code{\link[=install_jsonstructure_binary]{install_jsonstructure_binary()}}, \code{\link[=jsonstructure_binary_path]{jsonstructure_binary_path()}} -} diff --git a/r/man/jsonstructure_engine_version.Rd b/r/man/jsonstructure_engine_version.Rd new file mode 100644 index 0000000..fe62882 --- /dev/null +++ b/r/man/jsonstructure_engine_version.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/version.R +\name{jsonstructure_engine_version} +\alias{jsonstructure_engine_version} +\title{Version of the bundled JSON Structure C engine} +\usage{ +jsonstructure_engine_version() +} +\value{ +A single character string: the bundled engine's version. +} +\description{ +Reports the version string of the JSON Structure C engine that is compiled +into this package. The engine and its cJSON dependency are built from source +at install time; nothing is downloaded at run time. +} +\examples{ +jsonstructure_engine_version() +} diff --git a/r/man/jsonstructure_version.Rd b/r/man/jsonstructure_version.Rd index 8633de0..22a3c40 100644 --- a/r/man/jsonstructure_version.Rd +++ b/r/man/jsonstructure_version.Rd @@ -16,5 +16,5 @@ Package version jsonstructure_version() } \seealso{ -\code{\link[=jsonstructure_binary_version]{jsonstructure_binary_version()}} for the native engine version. +\code{\link[=jsonstructure_engine_version]{jsonstructure_engine_version()}} for the bundled C engine version. } diff --git a/r/src/Makevars b/r/src/Makevars index d159f7c..356a217 100644 --- a/r/src/Makevars +++ b/r/src/Makevars @@ -1,14 +1,12 @@ -# The shim loads the prebuilt json_structure library at runtime through the -# platform dynamic loader (dlopen/dlsym). On Linux and Solaris those symbols -# require linking libdl; on macOS and the BSDs they are part of libc/libSystem -# and no extra library is needed. The conditional below requires GNU make, -# which is declared in DESCRIPTION (SystemRequirements). -UNAME_S := $(shell uname -s) - -ifeq ($(UNAME_S),Linux) -PKG_LIBS = -ldl -endif - -ifeq ($(UNAME_S),SunOS) -PKG_LIBS = -ldl -endif +# Build the vendored JSON Structure C engine (and its cJSON dependency) from +# source, directly into the package shared object. Header lookups need this +# directory -- for the engine's and "json_structure/*.h" +# includes -- plus the cjson/ subdirectory for cJSON.c's own "cJSON.h". +# +# The engine serialises its allocator with pthread primitives on Unix +# (pthread_mutex + pthread_once in types.c), so the platform pthread flag is +# applied to both compilation and linking. The regex matcher is pure C +# (regex_utils.c) with no threading. R compiles every *.c in this directory +# automatically; the whole package is C (no libstdc++). +PKG_CPPFLAGS = -I. -Icjson $(SHLIB_PTHREAD_FLAGS) +PKG_LIBS = $(SHLIB_PTHREAD_FLAGS) diff --git a/r/src/Makevars.win b/r/src/Makevars.win index 3e0fc43..48a5d67 100644 --- a/r/src/Makevars.win +++ b/r/src/Makevars.win @@ -1,4 +1,11 @@ -# On Windows the shim uses LoadLibraryEx/GetProcAddress from kernel32, which is -# linked automatically. No extra libraries are required (and we must not -# inherit the Unix -ldl flag). +# Build the vendored JSON Structure C engine (and its cJSON dependency) from +# source, directly into the package shared object. Header lookups need this +# directory -- for the engine's and "json_structure/*.h" +# includes -- plus the cjson/ subdirectory for cJSON.c's own "cJSON.h". +# +# On Windows the engine serialises its allocator with Win32 SRWLOCK (from +# windows.h, which the toolchain links automatically), and the regex matcher is +# pure C (regex_utils.c) with no threading, so no extra libraries are required. +# The whole package is C, so R links it with the C toolchain (no libstdc++). +PKG_CPPFLAGS = -I. -Icjson PKG_LIBS = diff --git a/r/src/cJSON.c b/r/src/cJSON.c new file mode 100644 index 0000000..61483d9 --- /dev/null +++ b/r/src/cJSON.c @@ -0,0 +1,3143 @@ +/* + Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +/* cJSON */ +/* JSON parser in C. */ + +/* disable warnings about old C89 functions in MSVC */ +#if !defined(_CRT_SECURE_NO_DEPRECATE) && defined(_MSC_VER) +#define _CRT_SECURE_NO_DEPRECATE +#endif + +#ifdef __GNUC__ +#pragma GCC visibility push(default) +#endif +#if defined(_MSC_VER) +#pragma warning (push) +/* disable warning about single line comments in system headers */ +#pragma warning (disable : 4001) +#endif + +#include +#include +#include +#include +#include +#include +#include + +#ifdef ENABLE_LOCALES +#include +#endif + +#if defined(_MSC_VER) +#pragma warning (pop) +#endif +#ifdef __GNUC__ +#pragma GCC visibility pop +#endif + +#include "cJSON.h" + +/* define our own boolean type */ +#ifdef true +#undef true +#endif +#define true ((cJSON_bool)1) + +#ifdef false +#undef false +#endif +#define false ((cJSON_bool)0) + +/* define isnan and isinf for ANSI C, if in C99 or above, isnan and isinf has been defined in math.h */ +#ifndef isinf +#define isinf(d) (isnan((d - d)) && !isnan(d)) +#endif +#ifndef isnan +#define isnan(d) (d != d) +#endif + +#ifndef NAN +#ifdef _WIN32 +#define NAN sqrt(-1.0) +#else +#define NAN 0.0/0.0 +#endif +#endif + +typedef struct { + const unsigned char *json; + size_t position; +} error; +static error global_error = { NULL, 0 }; + +CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void) +{ + return (const char*) (global_error.json + global_error.position); +} + +CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item) +{ + if (!cJSON_IsString(item)) + { + return NULL; + } + + return item->valuestring; +} + +CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item) +{ + if (!cJSON_IsNumber(item)) + { + return (double) NAN; + } + + return item->valuedouble; +} + +/* This is a safeguard to prevent copy-pasters from using incompatible C and header files */ +#if (CJSON_VERSION_MAJOR != 1) || (CJSON_VERSION_MINOR != 7) || (CJSON_VERSION_PATCH != 18) + #error cJSON.h and cJSON.c have different versions. Make sure that both have the same. +#endif + +CJSON_PUBLIC(const char*) cJSON_Version(void) +{ + static char version[15]; + sprintf(version, "%i.%i.%i", CJSON_VERSION_MAJOR, CJSON_VERSION_MINOR, CJSON_VERSION_PATCH); + + return version; +} + +/* Case insensitive string comparison, doesn't consider two NULL pointers equal though */ +static int case_insensitive_strcmp(const unsigned char *string1, const unsigned char *string2) +{ + if ((string1 == NULL) || (string2 == NULL)) + { + return 1; + } + + if (string1 == string2) + { + return 0; + } + + for(; tolower(*string1) == tolower(*string2); (void)string1++, string2++) + { + if (*string1 == '\0') + { + return 0; + } + } + + return tolower(*string1) - tolower(*string2); +} + +typedef struct internal_hooks +{ + void *(CJSON_CDECL *allocate)(size_t size); + void (CJSON_CDECL *deallocate)(void *pointer); + void *(CJSON_CDECL *reallocate)(void *pointer, size_t size); +} internal_hooks; + +#if defined(_MSC_VER) +/* work around MSVC error C2322: '...' address of dllimport '...' is not static */ +static void * CJSON_CDECL internal_malloc(size_t size) +{ + return malloc(size); +} +static void CJSON_CDECL internal_free(void *pointer) +{ + free(pointer); +} +static void * CJSON_CDECL internal_realloc(void *pointer, size_t size) +{ + return realloc(pointer, size); +} +#else +#define internal_malloc malloc +#define internal_free free +#define internal_realloc realloc +#endif + +/* strlen of character literals resolved at compile time */ +#define static_strlen(string_literal) (sizeof(string_literal) - sizeof("")) + +static internal_hooks global_hooks = { internal_malloc, internal_free, internal_realloc }; + +static unsigned char* cJSON_strdup(const unsigned char* string, const internal_hooks * const hooks) +{ + size_t length = 0; + unsigned char *copy = NULL; + + if (string == NULL) + { + return NULL; + } + + length = strlen((const char*)string) + sizeof(""); + copy = (unsigned char*)hooks->allocate(length); + if (copy == NULL) + { + return NULL; + } + memcpy(copy, string, length); + + return copy; +} + +CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks) +{ + if (hooks == NULL) + { + /* Reset hooks */ + global_hooks.allocate = malloc; + global_hooks.deallocate = free; + global_hooks.reallocate = realloc; + return; + } + + global_hooks.allocate = malloc; + if (hooks->malloc_fn != NULL) + { + global_hooks.allocate = hooks->malloc_fn; + } + + global_hooks.deallocate = free; + if (hooks->free_fn != NULL) + { + global_hooks.deallocate = hooks->free_fn; + } + + /* use realloc only if both free and malloc are used */ + global_hooks.reallocate = NULL; + if ((global_hooks.allocate == malloc) && (global_hooks.deallocate == free)) + { + global_hooks.reallocate = realloc; + } +} + +/* Internal constructor. */ +static cJSON *cJSON_New_Item(const internal_hooks * const hooks) +{ + cJSON* node = (cJSON*)hooks->allocate(sizeof(cJSON)); + if (node) + { + memset(node, '\0', sizeof(cJSON)); + } + + return node; +} + +/* Delete a cJSON structure. */ +CJSON_PUBLIC(void) cJSON_Delete(cJSON *item) +{ + cJSON *next = NULL; + while (item != NULL) + { + next = item->next; + if (!(item->type & cJSON_IsReference) && (item->child != NULL)) + { + cJSON_Delete(item->child); + } + if (!(item->type & cJSON_IsReference) && (item->valuestring != NULL)) + { + global_hooks.deallocate(item->valuestring); + item->valuestring = NULL; + } + if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) + { + global_hooks.deallocate(item->string); + item->string = NULL; + } + global_hooks.deallocate(item); + item = next; + } +} + +/* get the decimal point character of the current locale */ +static unsigned char get_decimal_point(void) +{ +#ifdef ENABLE_LOCALES + struct lconv *lconv = localeconv(); + return (unsigned char) lconv->decimal_point[0]; +#else + return '.'; +#endif +} + +typedef struct +{ + const unsigned char *content; + size_t length; + size_t offset; + size_t depth; /* How deeply nested (in arrays/objects) is the input at the current offset. */ + internal_hooks hooks; +} parse_buffer; + +/* check if the given size is left to read in a given parse buffer (starting with 1) */ +#define can_read(buffer, size) ((buffer != NULL) && (((buffer)->offset + size) <= (buffer)->length)) +/* check if the buffer can be accessed at the given index (starting with 0) */ +#define can_access_at_index(buffer, index) ((buffer != NULL) && (((buffer)->offset + index) < (buffer)->length)) +#define cannot_access_at_index(buffer, index) (!can_access_at_index(buffer, index)) +/* get a pointer to the buffer at the position */ +#define buffer_at_offset(buffer) ((buffer)->content + (buffer)->offset) + +/* Parse the input text to generate a number, and populate the result into item. */ +static cJSON_bool parse_number(cJSON * const item, parse_buffer * const input_buffer) +{ + double number = 0; + unsigned char *after_end = NULL; + unsigned char number_c_string[64]; + unsigned char decimal_point = get_decimal_point(); + size_t i = 0; + + if ((input_buffer == NULL) || (input_buffer->content == NULL)) + { + return false; + } + + /* copy the number into a temporary buffer and replace '.' with the decimal point + * of the current locale (for strtod) + * This also takes care of '\0' not necessarily being available for marking the end of the input */ + for (i = 0; (i < (sizeof(number_c_string) - 1)) && can_access_at_index(input_buffer, i); i++) + { + switch (buffer_at_offset(input_buffer)[i]) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '+': + case '-': + case 'e': + case 'E': + number_c_string[i] = buffer_at_offset(input_buffer)[i]; + break; + + case '.': + number_c_string[i] = decimal_point; + break; + + default: + goto loop_end; + } + } +loop_end: + number_c_string[i] = '\0'; + + number = strtod((const char*)number_c_string, (char**)&after_end); + if (number_c_string == after_end) + { + return false; /* parse_error */ + } + + item->valuedouble = number; + + /* use saturation in case of overflow */ + if (number >= INT_MAX) + { + item->valueint = INT_MAX; + } + else if (number <= (double)INT_MIN) + { + item->valueint = INT_MIN; + } + else + { + item->valueint = (int)number; + } + + item->type = cJSON_Number; + + input_buffer->offset += (size_t)(after_end - number_c_string); + return true; +} + +/* don't ask me, but the original cJSON_SetNumberValue returns an integer or double */ +CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number) +{ + if (number >= INT_MAX) + { + object->valueint = INT_MAX; + } + else if (number <= (double)INT_MIN) + { + object->valueint = INT_MIN; + } + else + { + object->valueint = (int)number; + } + + return object->valuedouble = number; +} + +/* Note: when passing a NULL valuestring, cJSON_SetValuestring treats this as an error and return NULL */ +CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring) +{ + char *copy = NULL; + /* if object's type is not cJSON_String or is cJSON_IsReference, it should not set valuestring */ + if ((object == NULL) || !(object->type & cJSON_String) || (object->type & cJSON_IsReference)) + { + return NULL; + } + /* return NULL if the object is corrupted or valuestring is NULL */ + if (object->valuestring == NULL || valuestring == NULL) + { + return NULL; + } + if (strlen(valuestring) <= strlen(object->valuestring)) + { + strcpy(object->valuestring, valuestring); + return object->valuestring; + } + copy = (char*) cJSON_strdup((const unsigned char*)valuestring, &global_hooks); + if (copy == NULL) + { + return NULL; + } + if (object->valuestring != NULL) + { + cJSON_free(object->valuestring); + } + object->valuestring = copy; + + return copy; +} + +typedef struct +{ + unsigned char *buffer; + size_t length; + size_t offset; + size_t depth; /* current nesting depth (for formatted printing) */ + cJSON_bool noalloc; + cJSON_bool format; /* is this print a formatted print */ + internal_hooks hooks; +} printbuffer; + +/* realloc printbuffer if necessary to have at least "needed" bytes more */ +static unsigned char* ensure(printbuffer * const p, size_t needed) +{ + unsigned char *newbuffer = NULL; + size_t newsize = 0; + + if ((p == NULL) || (p->buffer == NULL)) + { + return NULL; + } + + if ((p->length > 0) && (p->offset >= p->length)) + { + /* make sure that offset is valid */ + return NULL; + } + + if (needed > INT_MAX) + { + /* sizes bigger than INT_MAX are currently not supported */ + return NULL; + } + + needed += p->offset + 1; + if (needed <= p->length) + { + return p->buffer + p->offset; + } + + if (p->noalloc) { + return NULL; + } + + /* calculate new buffer size */ + if (needed > (INT_MAX / 2)) + { + /* overflow of int, use INT_MAX if possible */ + if (needed <= INT_MAX) + { + newsize = INT_MAX; + } + else + { + return NULL; + } + } + else + { + newsize = needed * 2; + } + + if (p->hooks.reallocate != NULL) + { + /* reallocate with realloc if available */ + newbuffer = (unsigned char*)p->hooks.reallocate(p->buffer, newsize); + if (newbuffer == NULL) + { + p->hooks.deallocate(p->buffer); + p->length = 0; + p->buffer = NULL; + + return NULL; + } + } + else + { + /* otherwise reallocate manually */ + newbuffer = (unsigned char*)p->hooks.allocate(newsize); + if (!newbuffer) + { + p->hooks.deallocate(p->buffer); + p->length = 0; + p->buffer = NULL; + + return NULL; + } + + memcpy(newbuffer, p->buffer, p->offset + 1); + p->hooks.deallocate(p->buffer); + } + p->length = newsize; + p->buffer = newbuffer; + + return newbuffer + p->offset; +} + +/* calculate the new length of the string in a printbuffer and update the offset */ +static void update_offset(printbuffer * const buffer) +{ + const unsigned char *buffer_pointer = NULL; + if ((buffer == NULL) || (buffer->buffer == NULL)) + { + return; + } + buffer_pointer = buffer->buffer + buffer->offset; + + buffer->offset += strlen((const char*)buffer_pointer); +} + +/* securely comparison of floating-point variables */ +static cJSON_bool compare_double(double a, double b) +{ + double maxVal = fabs(a) > fabs(b) ? fabs(a) : fabs(b); + return (fabs(a - b) <= maxVal * DBL_EPSILON); +} + +/* Render the number nicely from the given item into a string. */ +static cJSON_bool print_number(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + double d = item->valuedouble; + int length = 0; + size_t i = 0; + unsigned char number_buffer[26] = {0}; /* temporary buffer to print the number into */ + unsigned char decimal_point = get_decimal_point(); + double test = 0.0; + + if (output_buffer == NULL) + { + return false; + } + + /* This checks for NaN and Infinity */ + if (isnan(d) || isinf(d)) + { + length = sprintf((char*)number_buffer, "null"); + } + else if(d == (double)item->valueint) + { + length = sprintf((char*)number_buffer, "%d", item->valueint); + } + else + { + /* Try 15 decimal places of precision to avoid nonsignificant nonzero digits */ + length = sprintf((char*)number_buffer, "%1.15g", d); + + /* Check whether the original double can be recovered */ + if ((sscanf((char*)number_buffer, "%lg", &test) != 1) || !compare_double((double)test, d)) + { + /* If not, print with 17 decimal places of precision */ + length = sprintf((char*)number_buffer, "%1.17g", d); + } + } + + /* sprintf failed or buffer overrun occurred */ + if ((length < 0) || (length > (int)(sizeof(number_buffer) - 1))) + { + return false; + } + + /* reserve appropriate space in the output */ + output_pointer = ensure(output_buffer, (size_t)length + sizeof("")); + if (output_pointer == NULL) + { + return false; + } + + /* copy the printed number to the output and replace locale + * dependent decimal point with '.' */ + for (i = 0; i < ((size_t)length); i++) + { + if (number_buffer[i] == decimal_point) + { + output_pointer[i] = '.'; + continue; + } + + output_pointer[i] = number_buffer[i]; + } + output_pointer[i] = '\0'; + + output_buffer->offset += (size_t)length; + + return true; +} + +/* parse 4 digit hexadecimal number */ +static unsigned parse_hex4(const unsigned char * const input) +{ + unsigned int h = 0; + size_t i = 0; + + for (i = 0; i < 4; i++) + { + /* parse digit */ + if ((input[i] >= '0') && (input[i] <= '9')) + { + h += (unsigned int) input[i] - '0'; + } + else if ((input[i] >= 'A') && (input[i] <= 'F')) + { + h += (unsigned int) 10 + input[i] - 'A'; + } + else if ((input[i] >= 'a') && (input[i] <= 'f')) + { + h += (unsigned int) 10 + input[i] - 'a'; + } + else /* invalid */ + { + return 0; + } + + if (i < 3) + { + /* shift left to make place for the next nibble */ + h = h << 4; + } + } + + return h; +} + +/* converts a UTF-16 literal to UTF-8 + * A literal can be one or two sequences of the form \uXXXX */ +static unsigned char utf16_literal_to_utf8(const unsigned char * const input_pointer, const unsigned char * const input_end, unsigned char **output_pointer) +{ + long unsigned int codepoint = 0; + unsigned int first_code = 0; + const unsigned char *first_sequence = input_pointer; + unsigned char utf8_length = 0; + unsigned char utf8_position = 0; + unsigned char sequence_length = 0; + unsigned char first_byte_mark = 0; + + if ((input_end - first_sequence) < 6) + { + /* input ends unexpectedly */ + goto fail; + } + + /* get the first utf16 sequence */ + first_code = parse_hex4(first_sequence + 2); + + /* check that the code is valid */ + if (((first_code >= 0xDC00) && (first_code <= 0xDFFF))) + { + goto fail; + } + + /* UTF16 surrogate pair */ + if ((first_code >= 0xD800) && (first_code <= 0xDBFF)) + { + const unsigned char *second_sequence = first_sequence + 6; + unsigned int second_code = 0; + sequence_length = 12; /* \uXXXX\uXXXX */ + + if ((input_end - second_sequence) < 6) + { + /* input ends unexpectedly */ + goto fail; + } + + if ((second_sequence[0] != '\\') || (second_sequence[1] != 'u')) + { + /* missing second half of the surrogate pair */ + goto fail; + } + + /* get the second utf16 sequence */ + second_code = parse_hex4(second_sequence + 2); + /* check that the code is valid */ + if ((second_code < 0xDC00) || (second_code > 0xDFFF)) + { + /* invalid second half of the surrogate pair */ + goto fail; + } + + + /* calculate the unicode codepoint from the surrogate pair */ + codepoint = 0x10000 + (((first_code & 0x3FF) << 10) | (second_code & 0x3FF)); + } + else + { + sequence_length = 6; /* \uXXXX */ + codepoint = first_code; + } + + /* encode as UTF-8 + * takes at maximum 4 bytes to encode: + * 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx */ + if (codepoint < 0x80) + { + /* normal ascii, encoding 0xxxxxxx */ + utf8_length = 1; + } + else if (codepoint < 0x800) + { + /* two bytes, encoding 110xxxxx 10xxxxxx */ + utf8_length = 2; + first_byte_mark = 0xC0; /* 11000000 */ + } + else if (codepoint < 0x10000) + { + /* three bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx */ + utf8_length = 3; + first_byte_mark = 0xE0; /* 11100000 */ + } + else if (codepoint <= 0x10FFFF) + { + /* four bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx 10xxxxxx */ + utf8_length = 4; + first_byte_mark = 0xF0; /* 11110000 */ + } + else + { + /* invalid unicode codepoint */ + goto fail; + } + + /* encode as utf8 */ + for (utf8_position = (unsigned char)(utf8_length - 1); utf8_position > 0; utf8_position--) + { + /* 10xxxxxx */ + (*output_pointer)[utf8_position] = (unsigned char)((codepoint | 0x80) & 0xBF); + codepoint >>= 6; + } + /* encode first byte */ + if (utf8_length > 1) + { + (*output_pointer)[0] = (unsigned char)((codepoint | first_byte_mark) & 0xFF); + } + else + { + (*output_pointer)[0] = (unsigned char)(codepoint & 0x7F); + } + + *output_pointer += utf8_length; + + return sequence_length; + +fail: + return 0; +} + +/* Parse the input text into an unescaped cinput, and populate item. */ +static cJSON_bool parse_string(cJSON * const item, parse_buffer * const input_buffer) +{ + const unsigned char *input_pointer = buffer_at_offset(input_buffer) + 1; + const unsigned char *input_end = buffer_at_offset(input_buffer) + 1; + unsigned char *output_pointer = NULL; + unsigned char *output = NULL; + + /* not a string */ + if (buffer_at_offset(input_buffer)[0] != '\"') + { + goto fail; + } + + { + /* calculate approximate size of the output (overestimate) */ + size_t allocation_length = 0; + size_t skipped_bytes = 0; + while (((size_t)(input_end - input_buffer->content) < input_buffer->length) && (*input_end != '\"')) + { + /* is escape sequence */ + if (input_end[0] == '\\') + { + if ((size_t)(input_end + 1 - input_buffer->content) >= input_buffer->length) + { + /* prevent buffer overflow when last input character is a backslash */ + goto fail; + } + skipped_bytes++; + input_end++; + } + input_end++; + } + if (((size_t)(input_end - input_buffer->content) >= input_buffer->length) || (*input_end != '\"')) + { + goto fail; /* string ended unexpectedly */ + } + + /* This is at most how much we need for the output */ + allocation_length = (size_t) (input_end - buffer_at_offset(input_buffer)) - skipped_bytes; + output = (unsigned char*)input_buffer->hooks.allocate(allocation_length + sizeof("")); + if (output == NULL) + { + goto fail; /* allocation failure */ + } + } + + output_pointer = output; + /* loop through the string literal */ + while (input_pointer < input_end) + { + if (*input_pointer != '\\') + { + *output_pointer++ = *input_pointer++; + } + /* escape sequence */ + else + { + unsigned char sequence_length = 2; + if ((input_end - input_pointer) < 1) + { + goto fail; + } + + switch (input_pointer[1]) + { + case 'b': + *output_pointer++ = '\b'; + break; + case 'f': + *output_pointer++ = '\f'; + break; + case 'n': + *output_pointer++ = '\n'; + break; + case 'r': + *output_pointer++ = '\r'; + break; + case 't': + *output_pointer++ = '\t'; + break; + case '\"': + case '\\': + case '/': + *output_pointer++ = input_pointer[1]; + break; + + /* UTF-16 literal */ + case 'u': + sequence_length = utf16_literal_to_utf8(input_pointer, input_end, &output_pointer); + if (sequence_length == 0) + { + /* failed to convert UTF16-literal to UTF-8 */ + goto fail; + } + break; + + default: + goto fail; + } + input_pointer += sequence_length; + } + } + + /* zero terminate the output */ + *output_pointer = '\0'; + + item->type = cJSON_String; + item->valuestring = (char*)output; + + input_buffer->offset = (size_t) (input_end - input_buffer->content); + input_buffer->offset++; + + return true; + +fail: + if (output != NULL) + { + input_buffer->hooks.deallocate(output); + output = NULL; + } + + if (input_pointer != NULL) + { + input_buffer->offset = (size_t)(input_pointer - input_buffer->content); + } + + return false; +} + +/* Render the cstring provided to an escaped version that can be printed. */ +static cJSON_bool print_string_ptr(const unsigned char * const input, printbuffer * const output_buffer) +{ + const unsigned char *input_pointer = NULL; + unsigned char *output = NULL; + unsigned char *output_pointer = NULL; + size_t output_length = 0; + /* numbers of additional characters needed for escaping */ + size_t escape_characters = 0; + + if (output_buffer == NULL) + { + return false; + } + + /* empty string */ + if (input == NULL) + { + output = ensure(output_buffer, sizeof("\"\"")); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "\"\""); + + return true; + } + + /* set "flag" to 1 if something needs to be escaped */ + for (input_pointer = input; *input_pointer; input_pointer++) + { + switch (*input_pointer) + { + case '\"': + case '\\': + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + /* one character escape sequence */ + escape_characters++; + break; + default: + if (*input_pointer < 32) + { + /* UTF-16 escape sequence uXXXX */ + escape_characters += 5; + } + break; + } + } + output_length = (size_t)(input_pointer - input) + escape_characters; + + output = ensure(output_buffer, output_length + sizeof("\"\"")); + if (output == NULL) + { + return false; + } + + /* no characters have to be escaped */ + if (escape_characters == 0) + { + output[0] = '\"'; + memcpy(output + 1, input, output_length); + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; + } + + output[0] = '\"'; + output_pointer = output + 1; + /* copy the string */ + for (input_pointer = input; *input_pointer != '\0'; (void)input_pointer++, output_pointer++) + { + if ((*input_pointer > 31) && (*input_pointer != '\"') && (*input_pointer != '\\')) + { + /* normal character, copy */ + *output_pointer = *input_pointer; + } + else + { + /* character needs to be escaped */ + *output_pointer++ = '\\'; + switch (*input_pointer) + { + case '\\': + *output_pointer = '\\'; + break; + case '\"': + *output_pointer = '\"'; + break; + case '\b': + *output_pointer = 'b'; + break; + case '\f': + *output_pointer = 'f'; + break; + case '\n': + *output_pointer = 'n'; + break; + case '\r': + *output_pointer = 'r'; + break; + case '\t': + *output_pointer = 't'; + break; + default: + /* escape and print as unicode codepoint */ + sprintf((char*)output_pointer, "u%04x", *input_pointer); + output_pointer += 4; + break; + } + } + } + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; +} + +/* Invoke print_string_ptr (which is useful) on an item. */ +static cJSON_bool print_string(const cJSON * const item, printbuffer * const p) +{ + return print_string_ptr((unsigned char*)item->valuestring, p); +} + +/* Predeclare these prototypes. */ +static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer); +static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer); +static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer); +static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer); + +/* Utility to jump whitespace and cr/lf */ +static parse_buffer *buffer_skip_whitespace(parse_buffer * const buffer) +{ + if ((buffer == NULL) || (buffer->content == NULL)) + { + return NULL; + } + + if (cannot_access_at_index(buffer, 0)) + { + return buffer; + } + + while (can_access_at_index(buffer, 0) && (buffer_at_offset(buffer)[0] <= 32)) + { + buffer->offset++; + } + + if (buffer->offset == buffer->length) + { + buffer->offset--; + } + + return buffer; +} + +/* skip the UTF-8 BOM (byte order mark) if it is at the beginning of a buffer */ +static parse_buffer *skip_utf8_bom(parse_buffer * const buffer) +{ + if ((buffer == NULL) || (buffer->content == NULL) || (buffer->offset != 0)) + { + return NULL; + } + + if (can_access_at_index(buffer, 4) && (strncmp((const char*)buffer_at_offset(buffer), "\xEF\xBB\xBF", 3) == 0)) + { + buffer->offset += 3; + } + + return buffer; +} + +CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated) +{ + size_t buffer_length; + + if (NULL == value) + { + return NULL; + } + + /* Adding null character size due to require_null_terminated. */ + buffer_length = strlen(value) + sizeof(""); + + return cJSON_ParseWithLengthOpts(value, buffer_length, return_parse_end, require_null_terminated); +} + +/* Parse an object - create a new root, and populate. */ +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated) +{ + parse_buffer buffer = { 0, 0, 0, 0, { 0, 0, 0 } }; + cJSON *item = NULL; + + /* reset error position */ + global_error.json = NULL; + global_error.position = 0; + + if (value == NULL || 0 == buffer_length) + { + goto fail; + } + + buffer.content = (const unsigned char*)value; + buffer.length = buffer_length; + buffer.offset = 0; + buffer.hooks = global_hooks; + + item = cJSON_New_Item(&global_hooks); + if (item == NULL) /* memory fail */ + { + goto fail; + } + + if (!parse_value(item, buffer_skip_whitespace(skip_utf8_bom(&buffer)))) + { + /* parse failure. ep is set. */ + goto fail; + } + + /* if we require null-terminated JSON without appended garbage, skip and then check for a null terminator */ + if (require_null_terminated) + { + buffer_skip_whitespace(&buffer); + if ((buffer.offset >= buffer.length) || buffer_at_offset(&buffer)[0] != '\0') + { + goto fail; + } + } + if (return_parse_end) + { + *return_parse_end = (const char*)buffer_at_offset(&buffer); + } + + return item; + +fail: + if (item != NULL) + { + cJSON_Delete(item); + } + + if (value != NULL) + { + error local_error; + local_error.json = (const unsigned char*)value; + local_error.position = 0; + + if (buffer.offset < buffer.length) + { + local_error.position = buffer.offset; + } + else if (buffer.length > 0) + { + local_error.position = buffer.length - 1; + } + + if (return_parse_end != NULL) + { + *return_parse_end = (const char*)local_error.json + local_error.position; + } + + global_error = local_error; + } + + return NULL; +} + +/* Default options for cJSON_Parse */ +CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value) +{ + return cJSON_ParseWithOpts(value, 0, 0); +} + +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length) +{ + return cJSON_ParseWithLengthOpts(value, buffer_length, 0, 0); +} + +#define cjson_min(a, b) (((a) < (b)) ? (a) : (b)) + +static unsigned char *print(const cJSON * const item, cJSON_bool format, const internal_hooks * const hooks) +{ + static const size_t default_buffer_size = 256; + printbuffer buffer[1]; + unsigned char *printed = NULL; + + memset(buffer, 0, sizeof(buffer)); + + /* create buffer */ + buffer->buffer = (unsigned char*) hooks->allocate(default_buffer_size); + buffer->length = default_buffer_size; + buffer->format = format; + buffer->hooks = *hooks; + if (buffer->buffer == NULL) + { + goto fail; + } + + /* print the value */ + if (!print_value(item, buffer)) + { + goto fail; + } + update_offset(buffer); + + /* check if reallocate is available */ + if (hooks->reallocate != NULL) + { + printed = (unsigned char*) hooks->reallocate(buffer->buffer, buffer->offset + 1); + if (printed == NULL) { + goto fail; + } + buffer->buffer = NULL; + } + else /* otherwise copy the JSON over to a new buffer */ + { + printed = (unsigned char*) hooks->allocate(buffer->offset + 1); + if (printed == NULL) + { + goto fail; + } + memcpy(printed, buffer->buffer, cjson_min(buffer->length, buffer->offset + 1)); + printed[buffer->offset] = '\0'; /* just to be sure */ + + /* free the buffer */ + hooks->deallocate(buffer->buffer); + buffer->buffer = NULL; + } + + return printed; + +fail: + if (buffer->buffer != NULL) + { + hooks->deallocate(buffer->buffer); + buffer->buffer = NULL; + } + + if (printed != NULL) + { + hooks->deallocate(printed); + printed = NULL; + } + + return NULL; +} + +/* Render a cJSON item/entity/structure to text. */ +CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item) +{ + return (char*)print(item, true, &global_hooks); +} + +CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item) +{ + return (char*)print(item, false, &global_hooks); +} + +CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt) +{ + printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } }; + + if (prebuffer < 0) + { + return NULL; + } + + p.buffer = (unsigned char*)global_hooks.allocate((size_t)prebuffer); + if (!p.buffer) + { + return NULL; + } + + p.length = (size_t)prebuffer; + p.offset = 0; + p.noalloc = false; + p.format = fmt; + p.hooks = global_hooks; + + if (!print_value(item, &p)) + { + global_hooks.deallocate(p.buffer); + p.buffer = NULL; + return NULL; + } + + return (char*)p.buffer; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format) +{ + printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } }; + + if ((length < 0) || (buffer == NULL)) + { + return false; + } + + p.buffer = (unsigned char*)buffer; + p.length = (size_t)length; + p.offset = 0; + p.noalloc = true; + p.format = format; + p.hooks = global_hooks; + + return print_value(item, &p); +} + +/* Parser core - when encountering text, process appropriately. */ +static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer) +{ + if ((input_buffer == NULL) || (input_buffer->content == NULL)) + { + return false; /* no input */ + } + + /* parse the different types of values */ + /* null */ + if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "null", 4) == 0)) + { + item->type = cJSON_NULL; + input_buffer->offset += 4; + return true; + } + /* false */ + if (can_read(input_buffer, 5) && (strncmp((const char*)buffer_at_offset(input_buffer), "false", 5) == 0)) + { + item->type = cJSON_False; + input_buffer->offset += 5; + return true; + } + /* true */ + if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "true", 4) == 0)) + { + item->type = cJSON_True; + item->valueint = 1; + input_buffer->offset += 4; + return true; + } + /* string */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '\"')) + { + return parse_string(item, input_buffer); + } + /* number */ + if (can_access_at_index(input_buffer, 0) && ((buffer_at_offset(input_buffer)[0] == '-') || ((buffer_at_offset(input_buffer)[0] >= '0') && (buffer_at_offset(input_buffer)[0] <= '9')))) + { + return parse_number(item, input_buffer); + } + /* array */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '[')) + { + return parse_array(item, input_buffer); + } + /* object */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '{')) + { + return parse_object(item, input_buffer); + } + + return false; +} + +/* Render a value to text. */ +static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output = NULL; + + if ((item == NULL) || (output_buffer == NULL)) + { + return false; + } + + switch ((item->type) & 0xFF) + { + case cJSON_NULL: + output = ensure(output_buffer, 5); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "null"); + return true; + + case cJSON_False: + output = ensure(output_buffer, 6); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "false"); + return true; + + case cJSON_True: + output = ensure(output_buffer, 5); + if (output == NULL) + { + return false; + } + strcpy((char*)output, "true"); + return true; + + case cJSON_Number: + return print_number(item, output_buffer); + + case cJSON_Raw: + { + size_t raw_length = 0; + if (item->valuestring == NULL) + { + return false; + } + + raw_length = strlen(item->valuestring) + sizeof(""); + output = ensure(output_buffer, raw_length); + if (output == NULL) + { + return false; + } + memcpy(output, item->valuestring, raw_length); + return true; + } + + case cJSON_String: + return print_string(item, output_buffer); + + case cJSON_Array: + return print_array(item, output_buffer); + + case cJSON_Object: + return print_object(item, output_buffer); + + default: + return false; + } +} + +/* Build an array from input text. */ +static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer) +{ + cJSON *head = NULL; /* head of the linked list */ + cJSON *current_item = NULL; + + if (input_buffer->depth >= CJSON_NESTING_LIMIT) + { + return false; /* to deeply nested */ + } + input_buffer->depth++; + + if (buffer_at_offset(input_buffer)[0] != '[') + { + /* not an array */ + goto fail; + } + + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ']')) + { + /* empty array */ + goto success; + } + + /* check if we skipped to the end of the buffer */ + if (cannot_access_at_index(input_buffer, 0)) + { + input_buffer->offset--; + goto fail; + } + + /* step back to character in front of the first element */ + input_buffer->offset--; + /* loop through the comma separated array elements */ + do + { + /* allocate next item */ + cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); + if (new_item == NULL) + { + goto fail; /* allocation failure */ + } + + /* attach next item to list */ + if (head == NULL) + { + /* start the linked list */ + current_item = head = new_item; + } + else + { + /* add to the end and advance */ + current_item->next = new_item; + new_item->prev = current_item; + current_item = new_item; + } + + /* parse next value */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_value(current_item, input_buffer)) + { + goto fail; /* failed to parse value */ + } + buffer_skip_whitespace(input_buffer); + } + while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); + + if (cannot_access_at_index(input_buffer, 0) || buffer_at_offset(input_buffer)[0] != ']') + { + goto fail; /* expected end of array */ + } + +success: + input_buffer->depth--; + + if (head != NULL) { + head->prev = current_item; + } + + item->type = cJSON_Array; + item->child = head; + + input_buffer->offset++; + + return true; + +fail: + if (head != NULL) + { + cJSON_Delete(head); + } + + return false; +} + +/* Render an array to text */ +static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + size_t length = 0; + cJSON *current_element = item->child; + + if (output_buffer == NULL) + { + return false; + } + + /* Compose the output array. */ + /* opening square bracket */ + output_pointer = ensure(output_buffer, 1); + if (output_pointer == NULL) + { + return false; + } + + *output_pointer = '['; + output_buffer->offset++; + output_buffer->depth++; + + while (current_element != NULL) + { + if (!print_value(current_element, output_buffer)) + { + return false; + } + update_offset(output_buffer); + if (current_element->next) + { + length = (size_t) (output_buffer->format ? 2 : 1); + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ','; + if(output_buffer->format) + { + *output_pointer++ = ' '; + } + *output_pointer = '\0'; + output_buffer->offset += length; + } + current_element = current_element->next; + } + + output_pointer = ensure(output_buffer, 2); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ']'; + *output_pointer = '\0'; + output_buffer->depth--; + + return true; +} + +/* Build an object from the text. */ +static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer) +{ + cJSON *head = NULL; /* linked list head */ + cJSON *current_item = NULL; + + if (input_buffer->depth >= CJSON_NESTING_LIMIT) + { + return false; /* to deeply nested */ + } + input_buffer->depth++; + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '{')) + { + goto fail; /* not an object */ + } + + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '}')) + { + goto success; /* empty object */ + } + + /* check if we skipped to the end of the buffer */ + if (cannot_access_at_index(input_buffer, 0)) + { + input_buffer->offset--; + goto fail; + } + + /* step back to character in front of the first element */ + input_buffer->offset--; + /* loop through the comma separated array elements */ + do + { + /* allocate next item */ + cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); + if (new_item == NULL) + { + goto fail; /* allocation failure */ + } + + /* attach next item to list */ + if (head == NULL) + { + /* start the linked list */ + current_item = head = new_item; + } + else + { + /* add to the end and advance */ + current_item->next = new_item; + new_item->prev = current_item; + current_item = new_item; + } + + if (cannot_access_at_index(input_buffer, 1)) + { + goto fail; /* nothing comes after the comma */ + } + + /* parse the name of the child */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_string(current_item, input_buffer)) + { + goto fail; /* failed to parse name */ + } + buffer_skip_whitespace(input_buffer); + + /* swap valuestring and string, because we parsed the name */ + current_item->string = current_item->valuestring; + current_item->valuestring = NULL; + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != ':')) + { + goto fail; /* invalid object */ + } + + /* parse the value */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_value(current_item, input_buffer)) + { + goto fail; /* failed to parse value */ + } + buffer_skip_whitespace(input_buffer); + } + while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '}')) + { + goto fail; /* expected end of object */ + } + +success: + input_buffer->depth--; + + if (head != NULL) { + head->prev = current_item; + } + + item->type = cJSON_Object; + item->child = head; + + input_buffer->offset++; + return true; + +fail: + if (head != NULL) + { + cJSON_Delete(head); + } + + return false; +} + +/* Render an object to text. */ +static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer) +{ + unsigned char *output_pointer = NULL; + size_t length = 0; + cJSON *current_item = item->child; + + if (output_buffer == NULL) + { + return false; + } + + /* Compose the output: */ + length = (size_t) (output_buffer->format ? 2 : 1); /* fmt: {\n */ + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + + *output_pointer++ = '{'; + output_buffer->depth++; + if (output_buffer->format) + { + *output_pointer++ = '\n'; + } + output_buffer->offset += length; + + while (current_item) + { + if (output_buffer->format) + { + size_t i; + output_pointer = ensure(output_buffer, output_buffer->depth); + if (output_pointer == NULL) + { + return false; + } + for (i = 0; i < output_buffer->depth; i++) + { + *output_pointer++ = '\t'; + } + output_buffer->offset += output_buffer->depth; + } + + /* print key */ + if (!print_string_ptr((unsigned char*)current_item->string, output_buffer)) + { + return false; + } + update_offset(output_buffer); + + length = (size_t) (output_buffer->format ? 2 : 1); + output_pointer = ensure(output_buffer, length); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ':'; + if (output_buffer->format) + { + *output_pointer++ = '\t'; + } + output_buffer->offset += length; + + /* print value */ + if (!print_value(current_item, output_buffer)) + { + return false; + } + update_offset(output_buffer); + + /* print comma if not last */ + length = ((size_t)(output_buffer->format ? 1 : 0) + (size_t)(current_item->next ? 1 : 0)); + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + if (current_item->next) + { + *output_pointer++ = ','; + } + + if (output_buffer->format) + { + *output_pointer++ = '\n'; + } + *output_pointer = '\0'; + output_buffer->offset += length; + + current_item = current_item->next; + } + + output_pointer = ensure(output_buffer, output_buffer->format ? (output_buffer->depth + 1) : 2); + if (output_pointer == NULL) + { + return false; + } + if (output_buffer->format) + { + size_t i; + for (i = 0; i < (output_buffer->depth - 1); i++) + { + *output_pointer++ = '\t'; + } + } + *output_pointer++ = '}'; + *output_pointer = '\0'; + output_buffer->depth--; + + return true; +} + +/* Get Array size/item / object item. */ +CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array) +{ + cJSON *child = NULL; + size_t size = 0; + + if (array == NULL) + { + return 0; + } + + child = array->child; + + while(child != NULL) + { + size++; + child = child->next; + } + + /* FIXME: Can overflow here. Cannot be fixed without breaking the API */ + + return (int)size; +} + +static cJSON* get_array_item(const cJSON *array, size_t index) +{ + cJSON *current_child = NULL; + + if (array == NULL) + { + return NULL; + } + + current_child = array->child; + while ((current_child != NULL) && (index > 0)) + { + index--; + current_child = current_child->next; + } + + return current_child; +} + +CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index) +{ + if (index < 0) + { + return NULL; + } + + return get_array_item(array, (size_t)index); +} + +static cJSON *get_object_item(const cJSON * const object, const char * const name, const cJSON_bool case_sensitive) +{ + cJSON *current_element = NULL; + + if ((object == NULL) || (name == NULL)) + { + return NULL; + } + + current_element = object->child; + if (case_sensitive) + { + while ((current_element != NULL) && (current_element->string != NULL) && (strcmp(name, current_element->string) != 0)) + { + current_element = current_element->next; + } + } + else + { + while ((current_element != NULL) && (case_insensitive_strcmp((const unsigned char*)name, (const unsigned char*)(current_element->string)) != 0)) + { + current_element = current_element->next; + } + } + + if ((current_element == NULL) || (current_element->string == NULL)) { + return NULL; + } + + return current_element; +} + +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string) +{ + return get_object_item(object, string, false); +} + +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string) +{ + return get_object_item(object, string, true); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string) +{ + return cJSON_GetObjectItem(object, string) ? 1 : 0; +} + +/* Utility for array list handling. */ +static void suffix_object(cJSON *prev, cJSON *item) +{ + prev->next = item; + item->prev = prev; +} + +/* Utility for handling references. */ +static cJSON *create_reference(const cJSON *item, const internal_hooks * const hooks) +{ + cJSON *reference = NULL; + if (item == NULL) + { + return NULL; + } + + reference = cJSON_New_Item(hooks); + if (reference == NULL) + { + return NULL; + } + + memcpy(reference, item, sizeof(cJSON)); + reference->string = NULL; + reference->type |= cJSON_IsReference; + reference->next = reference->prev = NULL; + return reference; +} + +static cJSON_bool add_item_to_array(cJSON *array, cJSON *item) +{ + cJSON *child = NULL; + + if ((item == NULL) || (array == NULL) || (array == item)) + { + return false; + } + + child = array->child; + /* + * To find the last item in array quickly, we use prev in array + */ + if (child == NULL) + { + /* list is empty, start new one */ + array->child = item; + item->prev = item; + item->next = NULL; + } + else + { + /* append to the end */ + if (child->prev) + { + suffix_object(child->prev, item); + array->child->prev = item; + } + } + + return true; +} + +/* Add item to array/object. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item) +{ + return add_item_to_array(array, item); +} + +#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) + #pragma GCC diagnostic push +#endif +#ifdef __GNUC__ +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif +/* helper function to cast away const */ +static void* cast_away_const(const void* string) +{ + return (void*)string; +} +#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) + #pragma GCC diagnostic pop +#endif + + +static cJSON_bool add_item_to_object(cJSON * const object, const char * const string, cJSON * const item, const internal_hooks * const hooks, const cJSON_bool constant_key) +{ + char *new_key = NULL; + int new_type = cJSON_Invalid; + + if ((object == NULL) || (string == NULL) || (item == NULL) || (object == item)) + { + return false; + } + + if (constant_key) + { + new_key = (char*)cast_away_const(string); + new_type = item->type | cJSON_StringIsConst; + } + else + { + new_key = (char*)cJSON_strdup((const unsigned char*)string, hooks); + if (new_key == NULL) + { + return false; + } + + new_type = item->type & ~cJSON_StringIsConst; + } + + if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) + { + hooks->deallocate(item->string); + } + + item->string = new_key; + item->type = new_type; + + return add_item_to_array(object, item); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item) +{ + return add_item_to_object(object, string, item, &global_hooks, false); +} + +/* Add an item to an object with constant string as key */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item) +{ + return add_item_to_object(object, string, item, &global_hooks, true); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item) +{ + if (array == NULL) + { + return false; + } + + return add_item_to_array(array, create_reference(item, &global_hooks)); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item) +{ + if ((object == NULL) || (string == NULL)) + { + return false; + } + + return add_item_to_object(object, string, create_reference(item, &global_hooks), &global_hooks, false); +} + +CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name) +{ + cJSON *null = cJSON_CreateNull(); + if (add_item_to_object(object, name, null, &global_hooks, false)) + { + return null; + } + + cJSON_Delete(null); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name) +{ + cJSON *true_item = cJSON_CreateTrue(); + if (add_item_to_object(object, name, true_item, &global_hooks, false)) + { + return true_item; + } + + cJSON_Delete(true_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name) +{ + cJSON *false_item = cJSON_CreateFalse(); + if (add_item_to_object(object, name, false_item, &global_hooks, false)) + { + return false_item; + } + + cJSON_Delete(false_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean) +{ + cJSON *bool_item = cJSON_CreateBool(boolean); + if (add_item_to_object(object, name, bool_item, &global_hooks, false)) + { + return bool_item; + } + + cJSON_Delete(bool_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number) +{ + cJSON *number_item = cJSON_CreateNumber(number); + if (add_item_to_object(object, name, number_item, &global_hooks, false)) + { + return number_item; + } + + cJSON_Delete(number_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string) +{ + cJSON *string_item = cJSON_CreateString(string); + if (add_item_to_object(object, name, string_item, &global_hooks, false)) + { + return string_item; + } + + cJSON_Delete(string_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw) +{ + cJSON *raw_item = cJSON_CreateRaw(raw); + if (add_item_to_object(object, name, raw_item, &global_hooks, false)) + { + return raw_item; + } + + cJSON_Delete(raw_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name) +{ + cJSON *object_item = cJSON_CreateObject(); + if (add_item_to_object(object, name, object_item, &global_hooks, false)) + { + return object_item; + } + + cJSON_Delete(object_item); + return NULL; +} + +CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name) +{ + cJSON *array = cJSON_CreateArray(); + if (add_item_to_object(object, name, array, &global_hooks, false)) + { + return array; + } + + cJSON_Delete(array); + return NULL; +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item) +{ + if ((parent == NULL) || (item == NULL)) + { + return NULL; + } + + if (item != parent->child) + { + /* not the first element */ + item->prev->next = item->next; + } + if (item->next != NULL) + { + /* not the last element */ + item->next->prev = item->prev; + } + + if (item == parent->child) + { + /* first element */ + parent->child = item->next; + } + else if (item->next == NULL) + { + /* last element */ + parent->child->prev = item->prev; + } + + /* make sure the detached item doesn't point anywhere anymore */ + item->prev = NULL; + item->next = NULL; + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which) +{ + if (which < 0) + { + return NULL; + } + + return cJSON_DetachItemViaPointer(array, get_array_item(array, (size_t)which)); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which) +{ + cJSON_Delete(cJSON_DetachItemFromArray(array, which)); +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string) +{ + cJSON *to_detach = cJSON_GetObjectItem(object, string); + + return cJSON_DetachItemViaPointer(object, to_detach); +} + +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string) +{ + cJSON *to_detach = cJSON_GetObjectItemCaseSensitive(object, string); + + return cJSON_DetachItemViaPointer(object, to_detach); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string) +{ + cJSON_Delete(cJSON_DetachItemFromObject(object, string)); +} + +CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string) +{ + cJSON_Delete(cJSON_DetachItemFromObjectCaseSensitive(object, string)); +} + +/* Replace array/object items with new ones. */ +CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem) +{ + cJSON *after_inserted = NULL; + + if (which < 0 || newitem == NULL) + { + return false; + } + + after_inserted = get_array_item(array, (size_t)which); + if (after_inserted == NULL) + { + return add_item_to_array(array, newitem); + } + + if (after_inserted != array->child && after_inserted->prev == NULL) { + /* return false if after_inserted is a corrupted array item */ + return false; + } + + newitem->next = after_inserted; + newitem->prev = after_inserted->prev; + after_inserted->prev = newitem; + if (after_inserted == array->child) + { + array->child = newitem; + } + else + { + newitem->prev->next = newitem; + } + return true; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement) +{ + if ((parent == NULL) || (parent->child == NULL) || (replacement == NULL) || (item == NULL)) + { + return false; + } + + if (replacement == item) + { + return true; + } + + replacement->next = item->next; + replacement->prev = item->prev; + + if (replacement->next != NULL) + { + replacement->next->prev = replacement; + } + if (parent->child == item) + { + if (parent->child->prev == parent->child) + { + replacement->prev = replacement; + } + parent->child = replacement; + } + else + { /* + * To find the last item in array quickly, we use prev in array. + * We can't modify the last item's next pointer where this item was the parent's child + */ + if (replacement->prev != NULL) + { + replacement->prev->next = replacement; + } + if (replacement->next == NULL) + { + parent->child->prev = replacement; + } + } + + item->next = NULL; + item->prev = NULL; + cJSON_Delete(item); + + return true; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem) +{ + if (which < 0) + { + return false; + } + + return cJSON_ReplaceItemViaPointer(array, get_array_item(array, (size_t)which), newitem); +} + +static cJSON_bool replace_item_in_object(cJSON *object, const char *string, cJSON *replacement, cJSON_bool case_sensitive) +{ + if ((replacement == NULL) || (string == NULL)) + { + return false; + } + + /* replace the name in the replacement */ + if (!(replacement->type & cJSON_StringIsConst) && (replacement->string != NULL)) + { + cJSON_free(replacement->string); + } + replacement->string = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks); + if (replacement->string == NULL) + { + return false; + } + + replacement->type &= ~cJSON_StringIsConst; + + return cJSON_ReplaceItemViaPointer(object, get_object_item(object, string, case_sensitive), replacement); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object, const char *string, cJSON *newitem) +{ + return replace_item_in_object(object, string, newitem, false); +} + +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object, const char *string, cJSON *newitem) +{ + return replace_item_in_object(object, string, newitem, true); +} + +/* Create basic types: */ +CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_NULL; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_True; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_False; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = boolean ? cJSON_True : cJSON_False; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_Number; + item->valuedouble = num; + + /* use saturation in case of overflow */ + if (num >= INT_MAX) + { + item->valueint = INT_MAX; + } + else if (num <= (double)INT_MIN) + { + item->valueint = INT_MIN; + } + else + { + item->valueint = (int)num; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_String; + item->valuestring = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks); + if(!item->valuestring) + { + cJSON_Delete(item); + return NULL; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) + { + item->type = cJSON_String | cJSON_IsReference; + item->valuestring = (char*)cast_away_const(string); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) { + item->type = cJSON_Object | cJSON_IsReference; + item->child = (cJSON*)cast_away_const(child); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child) { + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) { + item->type = cJSON_Array | cJSON_IsReference; + item->child = (cJSON*)cast_away_const(child); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type = cJSON_Raw; + item->valuestring = (char*)cJSON_strdup((const unsigned char*)raw, &global_hooks); + if(!item->valuestring) + { + cJSON_Delete(item); + return NULL; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if(item) + { + item->type=cJSON_Array; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item) + { + item->type = cJSON_Object; + } + + return item; +} + +/* Create Arrays: */ +CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber(numbers[i]); + if (!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber((double)numbers[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for(i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber(numbers[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (strings == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for (i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateString(strings[i]); + if(!n) + { + cJSON_Delete(a); + return NULL; + } + if(!i) + { + a->child = n; + } + else + { + suffix_object(p,n); + } + p = n; + } + + if (a && a->child) { + a->child->prev = n; + } + + return a; +} + +/* Duplication */ +CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse) +{ + cJSON *newitem = NULL; + cJSON *child = NULL; + cJSON *next = NULL; + cJSON *newchild = NULL; + + /* Bail on bad ptr */ + if (!item) + { + goto fail; + } + /* Create new item */ + newitem = cJSON_New_Item(&global_hooks); + if (!newitem) + { + goto fail; + } + /* Copy over all vars */ + newitem->type = item->type & (~cJSON_IsReference); + newitem->valueint = item->valueint; + newitem->valuedouble = item->valuedouble; + if (item->valuestring) + { + newitem->valuestring = (char*)cJSON_strdup((unsigned char*)item->valuestring, &global_hooks); + if (!newitem->valuestring) + { + goto fail; + } + } + if (item->string) + { + newitem->string = (item->type&cJSON_StringIsConst) ? item->string : (char*)cJSON_strdup((unsigned char*)item->string, &global_hooks); + if (!newitem->string) + { + goto fail; + } + } + /* If non-recursive, then we're done! */ + if (!recurse) + { + return newitem; + } + /* Walk the ->next chain for the child. */ + child = item->child; + while (child != NULL) + { + newchild = cJSON_Duplicate(child, true); /* Duplicate (with recurse) each item in the ->next chain */ + if (!newchild) + { + goto fail; + } + if (next != NULL) + { + /* If newitem->child already set, then crosswire ->prev and ->next and move on */ + next->next = newchild; + newchild->prev = next; + next = newchild; + } + else + { + /* Set newitem->child and move to it */ + newitem->child = newchild; + next = newchild; + } + child = child->next; + } + if (newitem && newitem->child) + { + newitem->child->prev = newchild; + } + + return newitem; + +fail: + if (newitem != NULL) + { + cJSON_Delete(newitem); + } + + return NULL; +} + +static void skip_oneline_comment(char **input) +{ + *input += static_strlen("//"); + + for (; (*input)[0] != '\0'; ++(*input)) + { + if ((*input)[0] == '\n') { + *input += static_strlen("\n"); + return; + } + } +} + +static void skip_multiline_comment(char **input) +{ + *input += static_strlen("/*"); + + for (; (*input)[0] != '\0'; ++(*input)) + { + if (((*input)[0] == '*') && ((*input)[1] == '/')) + { + *input += static_strlen("*/"); + return; + } + } +} + +static void minify_string(char **input, char **output) { + (*output)[0] = (*input)[0]; + *input += static_strlen("\""); + *output += static_strlen("\""); + + + for (; (*input)[0] != '\0'; (void)++(*input), ++(*output)) { + (*output)[0] = (*input)[0]; + + if ((*input)[0] == '\"') { + (*output)[0] = '\"'; + *input += static_strlen("\""); + *output += static_strlen("\""); + return; + } else if (((*input)[0] == '\\') && ((*input)[1] == '\"')) { + (*output)[1] = (*input)[1]; + *input += static_strlen("\""); + *output += static_strlen("\""); + } + } +} + +CJSON_PUBLIC(void) cJSON_Minify(char *json) +{ + char *into = json; + + if (json == NULL) + { + return; + } + + while (json[0] != '\0') + { + switch (json[0]) + { + case ' ': + case '\t': + case '\r': + case '\n': + json++; + break; + + case '/': + if (json[1] == '/') + { + skip_oneline_comment(&json); + } + else if (json[1] == '*') + { + skip_multiline_comment(&json); + } else { + json++; + } + break; + + case '\"': + minify_string(&json, (char**)&into); + break; + + default: + into[0] = json[0]; + json++; + into++; + } + } + + /* and null-terminate. */ + *into = '\0'; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Invalid; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_False; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xff) == cJSON_True; +} + + +CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & (cJSON_True | cJSON_False)) != 0; +} +CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_NULL; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Number; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_String; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Array; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Object; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Raw; +} + +CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive) +{ + if ((a == NULL) || (b == NULL) || ((a->type & 0xFF) != (b->type & 0xFF))) + { + return false; + } + + /* check if type is valid */ + switch (a->type & 0xFF) + { + case cJSON_False: + case cJSON_True: + case cJSON_NULL: + case cJSON_Number: + case cJSON_String: + case cJSON_Raw: + case cJSON_Array: + case cJSON_Object: + break; + + default: + return false; + } + + /* identical objects are equal */ + if (a == b) + { + return true; + } + + switch (a->type & 0xFF) + { + /* in these cases and equal type is enough */ + case cJSON_False: + case cJSON_True: + case cJSON_NULL: + return true; + + case cJSON_Number: + if (compare_double(a->valuedouble, b->valuedouble)) + { + return true; + } + return false; + + case cJSON_String: + case cJSON_Raw: + if ((a->valuestring == NULL) || (b->valuestring == NULL)) + { + return false; + } + if (strcmp(a->valuestring, b->valuestring) == 0) + { + return true; + } + + return false; + + case cJSON_Array: + { + cJSON *a_element = a->child; + cJSON *b_element = b->child; + + for (; (a_element != NULL) && (b_element != NULL);) + { + if (!cJSON_Compare(a_element, b_element, case_sensitive)) + { + return false; + } + + a_element = a_element->next; + b_element = b_element->next; + } + + /* one of the arrays is longer than the other */ + if (a_element != b_element) { + return false; + } + + return true; + } + + case cJSON_Object: + { + cJSON *a_element = NULL; + cJSON *b_element = NULL; + cJSON_ArrayForEach(a_element, a) + { + /* TODO This has O(n^2) runtime, which is horrible! */ + b_element = get_object_item(b, a_element->string, case_sensitive); + if (b_element == NULL) + { + return false; + } + + if (!cJSON_Compare(a_element, b_element, case_sensitive)) + { + return false; + } + } + + /* doing this twice, once on a and b to prevent true comparison if a subset of b + * TODO: Do this the proper way, this is just a fix for now */ + cJSON_ArrayForEach(b_element, b) + { + a_element = get_object_item(a, b_element->string, case_sensitive); + if (a_element == NULL) + { + return false; + } + + if (!cJSON_Compare(b_element, a_element, case_sensitive)) + { + return false; + } + } + + return true; + } + + default: + return false; + } +} + +CJSON_PUBLIC(void *) cJSON_malloc(size_t size) +{ + return global_hooks.allocate(size); +} + +CJSON_PUBLIC(void) cJSON_free(void *object) +{ + global_hooks.deallocate(object); + object = NULL; +} diff --git a/r/src/cjson/cJSON.h b/r/src/cjson/cJSON.h new file mode 100644 index 0000000..88cf0bc --- /dev/null +++ b/r/src/cjson/cJSON.h @@ -0,0 +1,300 @@ +/* + Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#ifndef cJSON__h +#define cJSON__h + +#ifdef __cplusplus +extern "C" +{ +#endif + +#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32)) +#define __WINDOWS__ +#endif + +#ifdef __WINDOWS__ + +/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options: + +CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols +CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default) +CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol + +For *nix builds that support visibility attribute, you can define similar behavior by + +setting default visibility to hidden by adding +-fvisibility=hidden (for gcc) +or +-xldscope=hidden (for sun cc) +to CFLAGS + +then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does + +*/ + +#define CJSON_CDECL __cdecl +#define CJSON_STDCALL __stdcall + +/* export symbols by default, this is necessary for copy pasting the C and header file */ +#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS) +#define CJSON_EXPORT_SYMBOLS +#endif + +#if defined(CJSON_HIDE_SYMBOLS) +#define CJSON_PUBLIC(type) type CJSON_STDCALL +#elif defined(CJSON_EXPORT_SYMBOLS) +#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL +#elif defined(CJSON_IMPORT_SYMBOLS) +#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL +#endif +#else /* !__WINDOWS__ */ +#define CJSON_CDECL +#define CJSON_STDCALL + +#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY) +#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type +#else +#define CJSON_PUBLIC(type) type +#endif +#endif + +/* project version */ +#define CJSON_VERSION_MAJOR 1 +#define CJSON_VERSION_MINOR 7 +#define CJSON_VERSION_PATCH 18 + +#include + +/* cJSON Types: */ +#define cJSON_Invalid (0) +#define cJSON_False (1 << 0) +#define cJSON_True (1 << 1) +#define cJSON_NULL (1 << 2) +#define cJSON_Number (1 << 3) +#define cJSON_String (1 << 4) +#define cJSON_Array (1 << 5) +#define cJSON_Object (1 << 6) +#define cJSON_Raw (1 << 7) /* raw json */ + +#define cJSON_IsReference 256 +#define cJSON_StringIsConst 512 + +/* The cJSON structure: */ +typedef struct cJSON +{ + /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */ + struct cJSON *next; + struct cJSON *prev; + /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */ + struct cJSON *child; + + /* The type of the item, as above. */ + int type; + + /* The item's string, if type==cJSON_String and type == cJSON_Raw */ + char *valuestring; + /* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */ + int valueint; + /* The item's number, if type==cJSON_Number */ + double valuedouble; + + /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */ + char *string; +} cJSON; + +typedef struct cJSON_Hooks +{ + /* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */ + void *(CJSON_CDECL *malloc_fn)(size_t sz); + void (CJSON_CDECL *free_fn)(void *ptr); +} cJSON_Hooks; + +typedef int cJSON_bool; + +/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them. + * This is to prevent stack overflows. */ +#ifndef CJSON_NESTING_LIMIT +#define CJSON_NESTING_LIMIT 1000 +#endif + +/* returns the version of cJSON as a string */ +CJSON_PUBLIC(const char*) cJSON_Version(void); + +/* Supply malloc, realloc and free functions to cJSON */ +CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks); + +/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */ +/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */ +CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value); +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length); +/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */ +/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */ +CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated); +CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated); + +/* Render a cJSON entity to text for transfer/storage. */ +CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item); +/* Render a cJSON entity to text for transfer/storage without any formatting. */ +CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item); +/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */ +CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt); +/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */ +/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */ +CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format); +/* Delete a cJSON entity and all subentities. */ +CJSON_PUBLIC(void) cJSON_Delete(cJSON *item); + +/* Returns the number of items in an array (or object). */ +CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array); +/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */ +CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index); +/* Get item "string" from object. Case insensitive. */ +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string); +CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string); +CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string); +/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */ +CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void); + +/* Check item type and return its value */ +CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item); +CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item); + +/* These functions check the type of an item */ +CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item); +CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item); + +/* These calls create a cJSON item of the appropriate type. */ +CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean); +CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num); +CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string); +/* raw json */ +CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw); +CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void); +CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void); + +/* Create a string where valuestring references a string so + * it will not be freed by cJSON_Delete */ +CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string); +/* Create an object/array that only references it's elements so + * they will not be freed by cJSON_Delete */ +CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child); +CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child); + +/* These utilities create an Array of count items. + * The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/ +CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count); +CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count); + +/* Append item to the specified array/object. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item); +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item); +/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object. + * WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before + * writing to `item->string` */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item); +/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */ +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item); +CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item); + +/* Remove/Detach items from Arrays/Objects. */ +CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which); +CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string); +CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string); +CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string); +CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string); + +/* Update array items. */ +CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */ +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem); +CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem); + +/* Duplicate a cJSON item */ +CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse); +/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will + * need to be released. With recurse!=0, it will duplicate any children connected to the item. + * The item->next and ->prev pointers are always zero on return from Duplicate. */ +/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal. + * case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */ +CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive); + +/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings. + * The input pointer json cannot point to a read-only address area, such as a string constant, + * but should point to a readable and writable address area. */ +CJSON_PUBLIC(void) cJSON_Minify(char *json); + +/* Helper functions for creating and adding items to an object at the same time. + * They return the added item or NULL on failure. */ +CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean); +CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number); +CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string); +CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw); +CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name); +CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name); + +/* When assigning an integer value, it needs to be propagated to valuedouble too. */ +#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number)) +/* helper for the cJSON_SetNumberValue macro */ +CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number); +#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number)) +/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */ +CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring); + +/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/ +#define cJSON_SetBoolValue(object, boolValue) ( \ + (object != NULL && ((object)->type & (cJSON_False|cJSON_True))) ? \ + (object)->type=((object)->type &(~(cJSON_False|cJSON_True)))|((boolValue)?cJSON_True:cJSON_False) : \ + cJSON_Invalid\ +) + +/* Macro for iterating over an array or object */ +#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next) + +/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */ +CJSON_PUBLIC(void *) cJSON_malloc(size_t size); +CJSON_PUBLIC(void) cJSON_free(void *object); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/r/src/error_codes.c b/r/src/error_codes.c new file mode 100644 index 0000000..131f45a --- /dev/null +++ b/r/src/error_codes.c @@ -0,0 +1,212 @@ +/** + * @file error_codes.c + * @brief Error code string representations + * + * Copyright (c) 2024 JSON Structure Contributors + * SPDX-License-Identifier: MIT + */ + +#include "json_structure/error_codes.h" + +/* Schema error messages */ +static const char* g_schema_error_messages[] = { + [JS_SCHEMA_OK] = "Schema is valid", + [JS_SCHEMA_NULL] = "Schema is null", + [JS_SCHEMA_INVALID_TYPE] = "Invalid schema type", + [JS_SCHEMA_MAX_DEPTH_EXCEEDED] = "Maximum schema nesting depth exceeded", + [JS_SCHEMA_ROOT_MISSING_ID] = "Root schema missing '$id' property", + [JS_SCHEMA_ROOT_MISSING_NAME] = "Root schema missing 'name' property", + [JS_SCHEMA_ROOT_MISSING_SCHEMA] = "Root schema missing '$schema' property", + [JS_SCHEMA_ROOT_MISSING_TYPE] = "Root schema missing 'type' property", + [JS_SCHEMA_KEYWORD_EMPTY] = "Keyword value cannot be empty", + [JS_SCHEMA_NAME_INVALID] = "Name must be a valid identifier", + + [JS_SCHEMA_TYPE_INVALID] = "Invalid type value", + [JS_SCHEMA_TYPE_NOT_STRING] = "Type must be a string", + [JS_SCHEMA_TYPE_ARRAY_EMPTY] = "Type array cannot be empty", + [JS_SCHEMA_TYPE_OBJECT_MISSING_REF] = "Object type reference is missing", + + [JS_SCHEMA_REF_NOT_FOUND] = "Referenced schema not found", + [JS_SCHEMA_REF_NOT_STRING] = "$ref must be a string", + [JS_SCHEMA_REF_CIRCULAR] = "Circular reference detected", + [JS_SCHEMA_REF_INVALID] = "Invalid reference format", + + [JS_SCHEMA_DEFINITIONS_MUST_BE_OBJECT] = "definitions must be an object", + [JS_SCHEMA_DEFINITION_MISSING_TYPE] = "Definition missing type", + [JS_SCHEMA_DEFINITION_INVALID] = "Invalid definition", + + [JS_SCHEMA_PROPERTIES_MUST_BE_OBJECT] = "Properties must be an object", + [JS_SCHEMA_PROPERTY_INVALID] = "Invalid property definition", + [JS_SCHEMA_REQUIRED_MUST_BE_ARRAY] = "Required must be an array", + [JS_SCHEMA_REQUIRED_ITEM_MUST_BE_STRING] = "Required items must be strings", + [JS_SCHEMA_REQUIRED_PROPERTY_NOT_DEFINED] = "Required property not defined in properties", + [JS_SCHEMA_ADDITIONAL_PROPERTIES_INVALID] = "Invalid additionalProperties value", + + [JS_SCHEMA_ARRAY_MISSING_ITEMS] = "Array type missing items definition", + [JS_SCHEMA_ITEMS_INVALID] = "Invalid items definition", + + [JS_SCHEMA_MAP_MISSING_VALUES] = "Map type missing values definition", + [JS_SCHEMA_VALUES_INVALID] = "Invalid values definition", + + [JS_SCHEMA_TUPLE_MISSING_DEFINITION] = "Tuple type missing definition", + [JS_SCHEMA_TUPLE_MISSING_PROPERTIES] = "Tuple missing properties", + [JS_SCHEMA_TUPLE_INVALID_FORMAT] = "Invalid tuple format", + [JS_SCHEMA_TUPLE_PROPERTY_NOT_DEFINED] = "Tuple property not defined", + + [JS_SCHEMA_CHOICE_MISSING_CHOICES] = "Choice type missing choices", + [JS_SCHEMA_CHOICES_NOT_OBJECT] = "Choices must be an object", + [JS_SCHEMA_CHOICE_INVALID] = "Invalid choice definition", + [JS_SCHEMA_SELECTOR_NOT_STRING] = "Selector must be a string", + + [JS_SCHEMA_ENUM_NOT_ARRAY] = "Enum must be an array", + [JS_SCHEMA_ENUM_EMPTY] = "Enum cannot be empty", + [JS_SCHEMA_ENUM_DUPLICATES] = "Enum contains duplicate values", + [JS_SCHEMA_CONST_INVALID] = "Invalid const value", + + [JS_SCHEMA_USES_NOT_ARRAY] = "$uses must be an array", + [JS_SCHEMA_USES_INVALID_EXTENSION] = "Invalid extension in $uses", + [JS_SCHEMA_OFFERS_NOT_ARRAY] = "$offers must be an array", + [JS_SCHEMA_OFFERS_INVALID_EXTENSION] = "Invalid extension in $offers", + [JS_SCHEMA_EXTENSION_KEYWORD_WITHOUT_USES] = "Extension keyword used without $uses declaration", + + [JS_SCHEMA_MIN_MAX_INVALID] = "Invalid minimum/maximum value", + [JS_SCHEMA_MINLENGTH_INVALID] = "Invalid minLength value", + [JS_SCHEMA_MAXLENGTH_INVALID] = "Invalid maxLength value", + [JS_SCHEMA_PATTERN_INVALID] = "Invalid pattern", + [JS_SCHEMA_FORMAT_INVALID] = "Invalid format", + [JS_SCHEMA_CONSTRAINT_TYPE_MISMATCH] = "Constraint not applicable to type", + [JS_SCHEMA_MINIMUM_EXCEEDS_MAXIMUM] = "Minimum exceeds maximum", + [JS_SCHEMA_MINLENGTH_EXCEEDS_MAXLENGTH] = "minLength exceeds maxLength", + [JS_SCHEMA_MINLENGTH_NEGATIVE] = "minLength cannot be negative", + [JS_SCHEMA_MAXLENGTH_NEGATIVE] = "maxLength cannot be negative", + [JS_SCHEMA_MINITEMS_EXCEEDS_MAXITEMS] = "minItems exceeds maxItems", + [JS_SCHEMA_MINITEMS_NEGATIVE] = "minItems cannot be negative", + [JS_SCHEMA_MULTIPLEOF_INVALID] = "multipleOf must be positive", + [JS_SCHEMA_KEYWORD_INVALID_TYPE] = "Keyword has invalid type", + [JS_SCHEMA_CONSTRAINT_VALUE_INVALID] = "Constraint value is invalid", + + [JS_SCHEMA_IMPORT_NOT_ALLOWED] = "$import not allowed", + [JS_SCHEMA_IMPORT_FAILED] = "Failed to import schema", + [JS_SCHEMA_IMPORT_CIRCULAR] = "Circular import detected", + + [JS_SCHEMA_ALLOF_NOT_ARRAY] = "allOf must be an array", + [JS_SCHEMA_ANYOF_NOT_ARRAY] = "anyOf must be an array", + [JS_SCHEMA_ONEOF_NOT_ARRAY] = "oneOf must be an array", + [JS_SCHEMA_NOT_INVALID] = "Invalid not schema", + [JS_SCHEMA_IF_INVALID] = "Invalid if schema", + [JS_SCHEMA_THEN_WITHOUT_IF] = "then without if", + [JS_SCHEMA_ELSE_WITHOUT_IF] = "else without if", +}; + +/* Instance error messages */ +static const char* g_instance_error_messages[] = { + [JS_INSTANCE_OK] = "Instance is valid", + + [JS_INSTANCE_TYPE_MISMATCH] = "Type mismatch", + [JS_INSTANCE_TYPE_UNKNOWN] = "Unknown type", + + [JS_INSTANCE_STRING_EXPECTED] = "Expected string", + [JS_INSTANCE_STRING_TOO_SHORT] = "String too short", + [JS_INSTANCE_STRING_TOO_LONG] = "String too long", + [JS_INSTANCE_STRING_PATTERN_MISMATCH] = "String does not match pattern", + [JS_INSTANCE_STRING_FORMAT_INVALID] = "Invalid string format", + + [JS_INSTANCE_NUMBER_EXPECTED] = "Expected number", + [JS_INSTANCE_NUMBER_TOO_SMALL] = "Number too small", + [JS_INSTANCE_NUMBER_TOO_LARGE] = "Number too large", + [JS_INSTANCE_NUMBER_NOT_MULTIPLE] = "Number not a multiple of required value", + [JS_INSTANCE_INTEGER_EXPECTED] = "Expected integer", + [JS_INSTANCE_INTEGER_OUT_OF_RANGE] = "Integer out of range", + + [JS_INSTANCE_BOOLEAN_EXPECTED] = "Expected boolean", + [JS_INSTANCE_NULL_EXPECTED] = "Expected null", + + [JS_INSTANCE_OBJECT_EXPECTED] = "Expected object", + [JS_INSTANCE_REQUIRED_MISSING] = "Required property missing", + [JS_INSTANCE_ADDITIONAL_PROPERTY] = "Additional property not allowed", + [JS_INSTANCE_PROPERTY_INVALID] = "Invalid property value", + [JS_INSTANCE_TOO_FEW_PROPERTIES] = "Too few properties", + [JS_INSTANCE_TOO_MANY_PROPERTIES] = "Too many properties", + [JS_INSTANCE_DEPENDENT_REQUIRED_MISSING] = "Dependent required property missing", + + [JS_INSTANCE_ARRAY_EXPECTED] = "Expected array", + [JS_INSTANCE_ARRAY_TOO_SHORT] = "Array too short", + [JS_INSTANCE_ARRAY_TOO_LONG] = "Array too long", + [JS_INSTANCE_ARRAY_NOT_UNIQUE] = "Array items not unique", + [JS_INSTANCE_ARRAY_CONTAINS_MISSING] = "Array does not contain required item", + [JS_INSTANCE_ARRAY_CONTAINS_TOO_FEW] = "Array contains too few matching items", + [JS_INSTANCE_ARRAY_CONTAINS_TOO_MANY] = "Array contains too many matching items", + [JS_INSTANCE_ARRAY_ITEM_INVALID] = "Invalid array item", + + [JS_INSTANCE_TUPLE_EXPECTED] = "Expected tuple", + [JS_INSTANCE_TUPLE_LENGTH_MISMATCH] = "Tuple length mismatch", + [JS_INSTANCE_TUPLE_ELEMENT_INVALID] = "Invalid tuple element", + + [JS_INSTANCE_MAP_EXPECTED] = "Expected map", + [JS_INSTANCE_MAP_VALUE_INVALID] = "Invalid map value", + [JS_INSTANCE_MAP_TOO_FEW_ENTRIES] = "Map has too few entries", + [JS_INSTANCE_MAP_TOO_MANY_ENTRIES] = "Map has too many entries", + [JS_INSTANCE_MAP_KEY_PATTERN_MISMATCH] = "Map key does not match pattern", + + [JS_INSTANCE_SET_EXPECTED] = "Expected set", + [JS_INSTANCE_SET_NOT_UNIQUE] = "Set items not unique", + [JS_INSTANCE_SET_ITEM_INVALID] = "Invalid set item", + + [JS_INSTANCE_CHOICE_NO_MATCH] = "No matching choice", + [JS_INSTANCE_CHOICE_MULTIPLE_MATCHES] = "Multiple matching choices", + [JS_INSTANCE_CHOICE_UNKNOWN] = "Unknown choice", + [JS_INSTANCE_CHOICE_SELECTOR_MISSING] = "Choice selector missing", + [JS_INSTANCE_CHOICE_SELECTOR_INVALID] = "Invalid choice selector", + + [JS_INSTANCE_ENUM_MISMATCH] = "Value not in enum", + [JS_INSTANCE_CONST_MISMATCH] = "Value does not match const", + + [JS_INSTANCE_DATE_EXPECTED] = "Expected date", + [JS_INSTANCE_DATE_INVALID] = "Invalid date format", + [JS_INSTANCE_TIME_EXPECTED] = "Expected time", + [JS_INSTANCE_TIME_INVALID] = "Invalid time format", + [JS_INSTANCE_DATETIME_EXPECTED] = "Expected datetime", + [JS_INSTANCE_DATETIME_INVALID] = "Invalid datetime format", + [JS_INSTANCE_DURATION_EXPECTED] = "Expected duration", + [JS_INSTANCE_DURATION_INVALID] = "Invalid duration format", + + [JS_INSTANCE_UUID_EXPECTED] = "Expected UUID", + [JS_INSTANCE_UUID_INVALID] = "Invalid UUID format", + + [JS_INSTANCE_URI_EXPECTED] = "Expected URI", + [JS_INSTANCE_URI_INVALID] = "Invalid URI format", + + [JS_INSTANCE_BINARY_EXPECTED] = "Expected binary", + [JS_INSTANCE_BINARY_INVALID] = "Invalid binary encoding", + + [JS_INSTANCE_JSONPOINTER_EXPECTED] = "Expected JSON Pointer", + [JS_INSTANCE_JSONPOINTER_INVALID] = "Invalid JSON Pointer format", + + [JS_INSTANCE_ALLOF_FAILED] = "allOf validation failed", + [JS_INSTANCE_ANYOF_FAILED] = "anyOf validation failed", + [JS_INSTANCE_ONEOF_FAILED] = "oneOf validation failed", + [JS_INSTANCE_ONEOF_MULTIPLE] = "Multiple oneOf schemas matched", + [JS_INSTANCE_NOT_FAILED] = "not validation failed", + [JS_INSTANCE_IF_THEN_FAILED] = "if-then validation failed", + [JS_INSTANCE_IF_ELSE_FAILED] = "if-else validation failed", + + [JS_INSTANCE_REF_NOT_FOUND] = "Referenced schema not found", + + [JS_INSTANCE_UNION_NO_MATCH] = "No matching union type", +}; + +const char* js_schema_error_str(js_schema_error_t code) { + if (code < 0 || code >= JS_SCHEMA_ERROR_COUNT) { + return "Unknown error"; + } + const char* msg = g_schema_error_messages[code]; + return msg ? msg : "Unknown error"; +} + +const char* js_instance_error_str(js_instance_error_t code) { + if (code < 0 || code >= JS_INSTANCE_ERROR_COUNT) { + return "Unknown error"; + } + const char* msg = g_instance_error_messages[code]; + return msg ? msg : "Unknown error"; +} diff --git a/r/src/init.c b/r/src/init.c index 6c58124..55d1525 100644 --- a/r/src/init.c +++ b/r/src/init.c @@ -1,31 +1,33 @@ /* * init.c - Registration of native routines for the jsonstructure package. + * + * The JSON Structure C engine is compiled into this package, so R_init also + * performs the engine's one-time initialization when the shared object loads. + * * SPDX-License-Identifier: MIT */ #include #include #include -#include -extern SEXP r_load_library(SEXP); -extern SEXP r_unload_library(void); -extern SEXP r_binding_loaded(void); -extern SEXP r_binding_version(void); +#include "json_structure/json_structure.h" + extern SEXP r_validate_schema(SEXP); extern SEXP r_validate_instance(SEXP, SEXP); +extern SEXP r_engine_version(void); +extern SEXP r_engine_cleanup(void); static const R_CallMethodDef CallEntries[] = { - {"load_library", (DL_FUNC) &r_load_library, 1}, - {"unload_library", (DL_FUNC) &r_unload_library, 0}, - {"binding_loaded", (DL_FUNC) &r_binding_loaded, 0}, - {"binding_version", (DL_FUNC) &r_binding_version, 0}, - {"validate_schema", (DL_FUNC) &r_validate_schema, 1}, - {"validate_instance",(DL_FUNC) &r_validate_instance,2}, + {"validate_schema", (DL_FUNC) &r_validate_schema, 1}, + {"validate_instance", (DL_FUNC) &r_validate_instance, 2}, + {"engine_version", (DL_FUNC) &r_engine_version, 0}, + {"engine_cleanup", (DL_FUNC) &r_engine_cleanup, 0}, {NULL, NULL, 0} }; void R_init_jsonstructure(DllInfo *dll) { + js_init(); R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); R_useDynamicSymbols(dll, FALSE); R_forceSymbols(dll, TRUE); diff --git a/r/src/instance_validator.c b/r/src/instance_validator.c new file mode 100644 index 0000000..d10597d --- /dev/null +++ b/r/src/instance_validator.c @@ -0,0 +1,1990 @@ +/** + * @file instance_validator.c + * @brief JSON Structure instance validator implementation + * + * Copyright (c) 2024 JSON Structure Contributors + * SPDX-License-Identifier: MIT + */ + +#include "json_structure/instance_validator.h" +#include "regex_utils.h" +#include +#include +#include +#include +#include +#include + +/* ============================================================================ + * Internal Constants + * ============================================================================ */ + +#define MAX_DEPTH 100 +#define PATH_BUFFER_SIZE 1024 +#define MAX_IMPORT_DEPTH 16 + +/* ============================================================================ + * Internal Types + * ============================================================================ */ + +typedef struct validate_context { + const js_instance_validator_t* validator; + js_result_t* result; + const cJSON* root_schema; + const cJSON* definitions; + char path[PATH_BUFFER_SIZE]; + int depth; +} validate_context_t; + +typedef struct import_context { + const js_instance_validator_t* validator; + js_result_t* result; + int import_depth; +} import_context_t; + +/* ============================================================================ + * Forward Declarations + * ============================================================================ */ + +static bool validate_instance(validate_context_t* ctx, const cJSON* instance, const cJSON* schema); +static bool validate_set_instance(validate_context_t* ctx, const cJSON* instance, const cJSON* schema); +static bool check_integer_range(validate_context_t* ctx, double value, const char* type_name); +static bool process_imports(import_context_t* ctx, cJSON* obj, const char* path); + +/* ============================================================================ + * Helper Functions + * ============================================================================ */ + +static void push_path(validate_context_t* ctx, const char* segment) { + size_t len = strlen(ctx->path); + size_t seg_len = strlen(segment); + size_t needed = seg_len + (len > 0 && segment[0] != '[' ? 1 : 0); /* +1 for dot separator */ + + /* Check for buffer overflow - silently truncate if path too long */ + if (len + needed >= PATH_BUFFER_SIZE) { + return; /* Path too long, skip appending */ + } + + /* Buffer space was validated above, so the segment is guaranteed to + * fit; copy directly to avoid -Wformat-truncation false positives that + * arise from snprintf's runtime bound. */ + if (len == 0) { + memcpy(ctx->path, segment, seg_len + 1); + } else if (segment[0] == '[') { + memcpy(ctx->path + len, segment, seg_len + 1); + } else { + ctx->path[len] = '.'; + memcpy(ctx->path + len + 1, segment, seg_len + 1); + } +} + +static void pop_path(validate_context_t* ctx, size_t prev_len) { + ctx->path[prev_len] = '\0'; +} + +static void add_error(validate_context_t* ctx, js_error_code_t code, const char* message) { + js_result_add_error(ctx->result, code, message, ctx->path); +} + +/* ============================================================================ + * Reference Resolution + * ============================================================================ */ + +static const cJSON* resolve_ref(validate_context_t* ctx, const char* ref) { + if (!ref) return NULL; + + /* Handle internal references */ + if (ref[0] == '#') { + /* Primary format: #/definitions/Name */ + if (strncmp(ref, "#/definitions/", 14) == 0) { + const char* def_name = ref + 14; + if (ctx->definitions) { + return cJSON_GetObjectItemCaseSensitive(ctx->definitions, def_name); + } + } + /* Also support: #/$defs/Name (JSON Schema compatibility) */ + if (strncmp(ref, "#/$defs/", 8) == 0) { + const char* def_name = ref + 8; + if (ctx->definitions) { + return cJSON_GetObjectItemCaseSensitive(ctx->definitions, def_name); + } + } + if (strncmp(ref, "#/$definitions/", 15) == 0) { + const char* def_name = ref + 15; + if (ctx->definitions) { + return cJSON_GetObjectItemCaseSensitive(ctx->definitions, def_name); + } + } + } + + return NULL; +} + +/* ============================================================================ + * Import Processing ($import and $importdefs) + * ============================================================================ */ + +/** + * @brief Fetch an external schema by URI from the import registry + * @param validator Validator with import registry + * @param uri URI to look up + * @return Parsed schema or NULL if not found + */ +static cJSON* fetch_external_schema(const js_instance_validator_t* validator, const char* uri) { + if (!validator || !uri) return NULL; + + const js_import_registry_t* registry = validator->options.import_registry; + if (!registry || !registry->entries) return NULL; + + for (size_t i = 0; i < registry->count; i++) { + const js_import_entry_t* entry = ®istry->entries[i]; + if (!entry->uri) continue; + + /* Check if URI matches */ + if (strcmp(entry->uri, uri) == 0) { + /* If schema is provided directly, duplicate it */ + if (entry->schema) { + return cJSON_Duplicate(entry->schema, true); + } + + /* If file path is provided, load from file */ + if (entry->file_path) { + FILE* f = fopen(entry->file_path, "rb"); + if (!f) return NULL; + + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + + if (size <= 0 || size > 10 * 1024 * 1024) { /* 10MB limit */ + fclose(f); + return NULL; + } + + char* content = (char*)malloc((size_t)size + 1); + if (!content) { + fclose(f); + return NULL; + } + + size_t read_size = fread(content, 1, (size_t)size, f); + fclose(f); + content[read_size] = '\0'; + + cJSON* schema = cJSON_Parse(content); + free(content); + return schema; + } + } + + /* Also check if schema's $id matches the requested URI */ + if (entry->schema) { + const cJSON* id = cJSON_GetObjectItemCaseSensitive(entry->schema, "$id"); + if (id && cJSON_IsString(id) && strcmp(id->valuestring, uri) == 0) { + return cJSON_Duplicate(entry->schema, true); + } + } + } + + return NULL; +} + +/** + * @brief Rewrite $ref pointers in imported content to point to new location + * @param obj Object to rewrite + * @param target_path Path where content is being merged (e.g., "#/definitions") + */ +static void rewrite_refs(cJSON* obj, const char* target_path) { + if (!obj || !target_path) return; + + if (cJSON_IsObject(obj)) { + cJSON* item = NULL; + cJSON_ArrayForEach(item, obj) { + if (strcmp(item->string, "$ref") == 0 && cJSON_IsString(item)) { + const char* ref = item->valuestring; + if (ref && ref[0] == '#') { + /* Extract the referenced name (last component) */ + const char* last_slash = strrchr(ref, '/'); + const char* ref_name = last_slash ? last_slash + 1 : ref + 1; + + if (ref_name && *ref_name) { + /* Build new ref: target_path/ref_name */ + size_t new_len = strlen(target_path) + 1 + strlen(ref_name) + 1; + char* new_ref = (char*)malloc(new_len); + if (new_ref) { + snprintf(new_ref, new_len, "%s/%s", target_path, ref_name); + cJSON_SetValuestring(item, new_ref); + free(new_ref); + } + } + } + } else if (strcmp(item->string, "$extends") == 0) { + /* $extends can be a string or array */ + if (cJSON_IsString(item)) { + const char* ref = item->valuestring; + if (ref && ref[0] == '#') { + const char* last_slash = strrchr(ref, '/'); + const char* ref_name = last_slash ? last_slash + 1 : ref + 1; + if (ref_name && *ref_name) { + size_t new_len = strlen(target_path) + 1 + strlen(ref_name) + 1; + char* new_ref = (char*)malloc(new_len); + if (new_ref) { + snprintf(new_ref, new_len, "%s/%s", target_path, ref_name); + cJSON_SetValuestring(item, new_ref); + free(new_ref); + } + } + } + } else if (cJSON_IsArray(item)) { + cJSON* arr_item = NULL; + cJSON_ArrayForEach(arr_item, item) { + if (cJSON_IsString(arr_item)) { + const char* ref = arr_item->valuestring; + if (ref && ref[0] == '#') { + const char* last_slash = strrchr(ref, '/'); + const char* ref_name = last_slash ? last_slash + 1 : ref + 1; + if (ref_name && *ref_name) { + size_t new_len = strlen(target_path) + 1 + strlen(ref_name) + 1; + char* new_ref = (char*)malloc(new_len); + if (new_ref) { + snprintf(new_ref, new_len, "%s/%s", target_path, ref_name); + cJSON_SetValuestring(arr_item, new_ref); + free(new_ref); + } + } + } + } + } + } + } else { + rewrite_refs(item, target_path); + } + } + } else if (cJSON_IsArray(obj)) { + cJSON* item = NULL; + cJSON_ArrayForEach(item, obj) { + rewrite_refs(item, target_path); + } + } +} + +/** + * @brief Process $import and $importdefs keywords in schema + * @param ctx Import context + * @param obj Schema object to process (modified in place) + * @param path Current JSON pointer path + * @return true if processing succeeded, false on error + */ +static bool process_imports(import_context_t* ctx, cJSON* obj, const char* path) { + if (!obj || !cJSON_IsObject(obj)) return true; + + /* Prevent infinite recursion */ + if (ctx->import_depth >= MAX_IMPORT_DEPTH) { + js_result_add_error(ctx->result, JS_SCHEMA_IMPORT_NOT_ALLOWED, + "Maximum import depth exceeded", path); + return false; + } + + /* Collect keys to process (we modify the object during iteration) */ + const char* import_key = NULL; + cJSON* import_value = NULL; + + cJSON* import_item = cJSON_GetObjectItemCaseSensitive(obj, "$import"); + if (import_item) { + import_key = "$import"; + import_value = import_item; + } else { + import_item = cJSON_GetObjectItemCaseSensitive(obj, "$importdefs"); + if (import_item) { + import_key = "$importdefs"; + import_value = import_item; + } + } + + if (import_key && import_value) { + /* Check if imports are allowed */ + if (!ctx->validator->options.allow_import) { + char msg[256]; + snprintf(msg, sizeof(msg), "Import keyword '%s' encountered but allow_import not enabled", import_key); + js_result_add_error(ctx->result, JS_SCHEMA_IMPORT_NOT_ALLOWED, msg, path); + return false; + } + + /* Validate URI */ + if (!cJSON_IsString(import_value)) { + char msg[256]; + snprintf(msg, sizeof(msg), "Import keyword '%s' value must be a string URI", import_key); + js_result_add_error(ctx->result, JS_SCHEMA_IMPORT_NOT_ALLOWED, msg, path); + return false; + } + + const char* uri = import_value->valuestring; + + /* Fetch external schema */ + cJSON* external = fetch_external_schema(ctx->validator, uri); + if (!external) { + char msg[256]; + snprintf(msg, sizeof(msg), "Unable to fetch external schema from URI: %s", uri); + js_result_add_error(ctx->result, JS_SCHEMA_IMPORT_NOT_ALLOWED, msg, path); + return false; + } + + /* Recursively process imports in the fetched schema */ + ctx->import_depth++; + bool import_result = process_imports(ctx, external, "#"); + ctx->import_depth--; + + if (!import_result) { + cJSON_Delete(external); + return false; + } + + /* Determine what to import */ + cJSON* imported_defs = cJSON_CreateObject(); + if (!imported_defs) { + cJSON_Delete(external); + return false; + } + + if (strcmp(import_key, "$import") == 0) { + /* $import: Import root type (if has type+name) and definitions */ + const cJSON* type_field = cJSON_GetObjectItemCaseSensitive(external, "type"); + const cJSON* name_field = cJSON_GetObjectItemCaseSensitive(external, "name"); + + if (type_field && name_field && cJSON_IsString(name_field)) { + /* Deep copy the external schema as a type definition */ + cJSON* ext_copy = cJSON_Duplicate(external, true); + if (ext_copy) { + cJSON_AddItemToObject(imported_defs, name_field->valuestring, ext_copy); + } + } + + /* Also import definitions */ + const cJSON* ext_defs = cJSON_GetObjectItemCaseSensitive(external, "definitions"); + if (!ext_defs) { + ext_defs = cJSON_GetObjectItemCaseSensitive(external, "$defs"); + } + if (ext_defs && cJSON_IsObject(ext_defs)) { + cJSON* def = NULL; + cJSON_ArrayForEach(def, ext_defs) { + if (def->string && !cJSON_GetObjectItemCaseSensitive(imported_defs, def->string)) { + cJSON* def_copy = cJSON_Duplicate(def, true); + if (def_copy) { + cJSON_AddItemToObject(imported_defs, def->string, def_copy); + } + } + } + } + } else { + /* $importdefs: Import only definitions */ + const cJSON* ext_defs = cJSON_GetObjectItemCaseSensitive(external, "definitions"); + if (!ext_defs) { + ext_defs = cJSON_GetObjectItemCaseSensitive(external, "$defs"); + } + if (ext_defs && cJSON_IsObject(ext_defs)) { + cJSON* def = NULL; + cJSON_ArrayForEach(def, ext_defs) { + if (def->string) { + cJSON* def_copy = cJSON_Duplicate(def, true); + if (def_copy) { + cJSON_AddItemToObject(imported_defs, def->string, def_copy); + } + } + } + } + } + + /* Determine merge target and ref rewrite path */ + const char* target_path; + cJSON* merge_target; + bool is_root = (strcmp(path, "#") == 0); + + if (is_root) { + target_path = "#/definitions"; + /* Ensure definitions object exists */ + cJSON* defs = cJSON_GetObjectItemCaseSensitive(obj, "definitions"); + if (!defs) { + defs = cJSON_CreateObject(); + if (defs) { + cJSON_AddItemToObject(obj, "definitions", defs); + } + } + merge_target = defs; + } else { + target_path = path; + merge_target = obj; + } + + /* Rewrite $ref pointers in imported content */ + cJSON* def = NULL; + cJSON_ArrayForEach(def, imported_defs) { + rewrite_refs(def, target_path); + } + + /* Merge imported definitions (don't overwrite existing) + First collect keys to merge, then merge them to avoid modifying during iteration */ + if (merge_target) { + /* Count and collect keys */ + int count = cJSON_GetArraySize(imported_defs); + if (count > 0) { + char** keys_to_merge = (char**)malloc(sizeof(char*) * (size_t)count); + if (keys_to_merge) { + int merge_count = 0; + cJSON* item = NULL; + cJSON_ArrayForEach(item, imported_defs) { + if (item->string && !cJSON_GetObjectItemCaseSensitive(merge_target, item->string)) { + keys_to_merge[merge_count++] = item->string; + } + } + + /* Now detach and merge */ + for (int i = 0; i < merge_count; i++) { + cJSON* detached = cJSON_DetachItemFromObject(imported_defs, keys_to_merge[i]); + if (detached) { + cJSON_AddItemToObject(merge_target, keys_to_merge[i], detached); + } + } + + free(keys_to_merge); + } + } + } + + /* Remove the import keyword from the schema */ + cJSON_DeleteItemFromObject(obj, import_key); + + /* Clean up */ + cJSON_Delete(imported_defs); + cJSON_Delete(external); + } + + /* Recursively process nested objects (but skip 'properties' values) */ + cJSON* child = NULL; + cJSON_ArrayForEach(child, obj) { + if (child->string && strcmp(child->string, "properties") == 0) { + continue; /* Don't process inside properties - those are property names */ + } + + if (cJSON_IsObject(child)) { + char child_path[PATH_BUFFER_SIZE]; + snprintf(child_path, sizeof(child_path), "%s/%s", path, child->string ? child->string : ""); + if (!process_imports(ctx, child, child_path)) { + return false; + } + } else if (cJSON_IsArray(child)) { + int idx = 0; + cJSON* arr_item = NULL; + cJSON_ArrayForEach(arr_item, child) { + if (cJSON_IsObject(arr_item)) { + char arr_path[PATH_BUFFER_SIZE]; + snprintf(arr_path, sizeof(arr_path), "%s/%s[%d]", path, child->string ? child->string : "", idx); + if (!process_imports(ctx, arr_item, arr_path)) { + return false; + } + } + idx++; + } + } + } + + return true; +} + +/* ============================================================================ + * Efficient uniqueItems Check with Hash-Based Optimization + * + * For primitive arrays (numbers, strings, bools), we use a simple hash set + * to achieve O(n) average case instead of O(n²) pairwise comparison. + * For mixed/complex arrays, we fall back to O(n²) cJSON_Compare. + * ============================================================================ */ + +/* Simple hash for primitive JSON values */ +static uint64_t hash_primitive(const cJSON* item) { + if (cJSON_IsNull(item)) return 0; + if (cJSON_IsBool(item)) return cJSON_IsTrue(item) ? 1 : 2; + if (cJSON_IsNumber(item)) { + /* Hash the double bits directly */ + union { double d; uint64_t u; } val; + val.d = item->valuedouble; + return val.u ^ 0x9e3779b97f4a7c15ULL; + } + if (cJSON_IsString(item) && item->valuestring) { + /* djb2 hash for strings */ + uint64_t hash = 5381; + const char* s = item->valuestring; + while (*s) { + hash = ((hash << 5) + hash) ^ (unsigned char)*s++; + } + return hash; + } + return 0xFFFFFFFFFFFFFFFFULL; /* Marker for complex types */ +} + +/* Check if array contains only hashable primitives */ +static bool is_primitive_array(const cJSON* arr) { + cJSON* item = NULL; + cJSON_ArrayForEach(item, arr) { + if (cJSON_IsObject(item) || cJSON_IsArray(item)) { + return false; + } + } + return true; +} + +/* Hash-based uniqueness check for primitive arrays - O(n) average */ +static bool check_unique_primitive(const cJSON* arr, int* dup_i, int* dup_j) { + int size = cJSON_GetArraySize(arr); + if (size <= 1) return true; + + /* Use a simple open-addressing hash table */ + size_t table_size = (size_t)size * 2; /* Load factor ~0.5 */ + if (table_size < 16) table_size = 16; + + /* Each slot: hash, index, occupied flag */ + typedef struct { uint64_t hash; int index; } slot_t; + slot_t* table = (slot_t*)calloc(table_size, sizeof(slot_t)); + if (!table) { + /* Fall back by signaling we couldn't check */ + *dup_i = -1; + return true; + } + + bool unique = true; + int i = 0; + cJSON* item = NULL; + cJSON_ArrayForEach(item, arr) { + uint64_t h = hash_primitive(item); + size_t pos = h % table_size; + + /* Linear probe for open slot or hash collision */ + for (size_t probe = 0; probe < table_size; probe++) { + size_t idx = (pos + probe) % table_size; + + if (table[idx].hash == 0 && table[idx].index == 0) { + /* Empty slot - insert */ + table[idx].hash = h ? h : 1; /* Avoid 0 as it marks empty */ + table[idx].index = i + 1; /* 1-indexed to distinguish from empty */ + break; + } + + if (table[idx].hash == (h ? h : 1)) { + /* Hash collision - check actual equality */ + cJSON* other = cJSON_GetArrayItem(arr, table[idx].index - 1); + if (cJSON_Compare(item, other, true)) { + *dup_i = table[idx].index - 1; + *dup_j = i; + unique = false; + break; + } + } + } + + if (!unique) break; + i++; + } + + free(table); + return unique; +} + +/* ============================================================================ + * Format Validation Helpers + * ============================================================================ */ + +/* Validate ISO 8601 datetime format: YYYY-MM-DDTHH:MM:SSZ or similar */ +static bool is_valid_datetime(const char* str) { + size_t len = strlen(str); + if (len < 20) return false; /* Minimum: 2024-01-01T00:00:00Z */ + + /* Check date part */ + if (str[4] != '-' || str[7] != '-') return false; + if (str[10] != 'T' && str[10] != 't') return false; + + /* Check year */ + for (int i = 0; i < 4; i++) { + if (!isdigit((unsigned char)str[i])) return false; + } + + /* Check month */ + if (!isdigit((unsigned char)str[5]) || !isdigit((unsigned char)str[6])) return false; + int month = (str[5] - '0') * 10 + (str[6] - '0'); + if (month < 1 || month > 12) return false; + + /* Check day */ + if (!isdigit((unsigned char)str[8]) || !isdigit((unsigned char)str[9])) return false; + int day = (str[8] - '0') * 10 + (str[9] - '0'); + if (day < 1 || day > 31) return false; + + /* Check time separator and time format */ + if (str[13] != ':' || str[16] != ':') return false; + + /* Check hours */ + if (!isdigit((unsigned char)str[11]) || !isdigit((unsigned char)str[12])) return false; + int hour = (str[11] - '0') * 10 + (str[12] - '0'); + if (hour > 23) return false; + + /* Check minutes */ + if (!isdigit((unsigned char)str[14]) || !isdigit((unsigned char)str[15])) return false; + int minute = (str[14] - '0') * 10 + (str[15] - '0'); + if (minute > 59) return false; + + /* Check seconds */ + if (!isdigit((unsigned char)str[17]) || !isdigit((unsigned char)str[18])) return false; + int second = (str[17] - '0') * 10 + (str[18] - '0'); + if (second > 59) return false; + + /* Check timezone - either Z or +/-HH:MM */ + size_t tz_start = 19; + if (str[tz_start] == '.') { + /* Skip fractional seconds */ + tz_start++; + while (tz_start < len && isdigit((unsigned char)str[tz_start])) { + tz_start++; + } + } + + if (tz_start >= len) return false; + + if (str[tz_start] == 'Z' || str[tz_start] == 'z') { + return tz_start + 1 == len; + } + + if (str[tz_start] == '+' || str[tz_start] == '-') { + /* Check offset format: +HH:MM or +HHMM */ + if (len - tz_start >= 6 && str[tz_start + 3] == ':') { + return isdigit((unsigned char)str[tz_start + 1]) && + isdigit((unsigned char)str[tz_start + 2]) && + isdigit((unsigned char)str[tz_start + 4]) && + isdigit((unsigned char)str[tz_start + 5]); + } + } + + return false; +} + +/* Validate UUID format: 8-4-4-4-12 hex digits */ +static bool is_valid_uuid(const char* str) { + size_t len = strlen(str); + if (len != 36) return false; + + const int segments[] = {8, 4, 4, 4, 12}; + int pos = 0; + + for (int seg = 0; seg < 5; seg++) { + for (int i = 0; i < segments[seg]; i++) { + char c = str[pos++]; + if (!isxdigit((unsigned char)c)) return false; + } + if (seg < 4) { + if (str[pos++] != '-') return false; + } + } + + return pos == 36; +} + +/* ============================================================================ + * Type Validation + * ============================================================================ */ + +/** + * @brief Check if instance matches the expected JSON Structure type + * + * Uses js_type_from_name for O(n) type string lookup once, then O(1) switch. + * This is more efficient than the previous chain of strcmp calls. + */ +static bool check_type_match(const cJSON* instance, const char* type_name) { + js_type_t type = js_type_from_name(type_name); + + switch (type) { + case JS_TYPE_ANY: + return true; + + case JS_TYPE_NULL: + return cJSON_IsNull(instance); + + case JS_TYPE_BOOLEAN: + return cJSON_IsBool(instance); + + case JS_TYPE_NUMBER: + return cJSON_IsNumber(instance); + + case JS_TYPE_INTEGER: + if (!cJSON_IsNumber(instance)) return false; + return instance->valuedouble == floor(instance->valuedouble); + + case JS_TYPE_STRING: + return cJSON_IsString(instance); + + case JS_TYPE_OBJECT: + return cJSON_IsObject(instance); + + case JS_TYPE_ARRAY: + return cJSON_IsArray(instance); + + case JS_TYPE_MAP: + return cJSON_IsObject(instance); + + case JS_TYPE_SET: + return cJSON_IsArray(instance); + + /* Integer numeric types - require integer value */ + case JS_TYPE_INT8: + case JS_TYPE_INT16: + case JS_TYPE_INT32: + case JS_TYPE_INT64: + case JS_TYPE_UINT8: + case JS_TYPE_UINT16: + case JS_TYPE_UINT32: + case JS_TYPE_UINT64: + if (!cJSON_IsNumber(instance)) return false; + return instance->valuedouble == floor(instance->valuedouble); + + /* Floating point numeric types */ + case JS_TYPE_FLOAT16: + case JS_TYPE_FLOAT32: + case JS_TYPE_FLOAT64: + case JS_TYPE_FLOAT128: + case JS_TYPE_DECIMAL: + case JS_TYPE_DECIMAL64: + case JS_TYPE_DECIMAL128: + return cJSON_IsNumber(instance); + + /* Datetime with format validation */ + case JS_TYPE_DATETIME: + if (!cJSON_IsString(instance)) return false; + return is_valid_datetime(instance->valuestring); + + /* UUID with format validation */ + case JS_TYPE_UUID: + if (!cJSON_IsString(instance)) return false; + return is_valid_uuid(instance->valuestring); + + /* String-based types without strict format validation */ + case JS_TYPE_BINARY: + case JS_TYPE_DATE: + case JS_TYPE_TIME: + case JS_TYPE_DURATION: + case JS_TYPE_URI: + case JS_TYPE_URI_REFERENCE: + case JS_TYPE_URI_TEMPLATE: + case JS_TYPE_REGEX: + case JS_TYPE_CHAR: + case JS_TYPE_IPV4: + case JS_TYPE_IPV6: + case JS_TYPE_EMAIL: + case JS_TYPE_IDN_EMAIL: + case JS_TYPE_HOSTNAME: + case JS_TYPE_IDN_HOSTNAME: + case JS_TYPE_IRI: + case JS_TYPE_IRI_REFERENCE: + case JS_TYPE_JSON_POINTER: + case JS_TYPE_RELATIVE_JSON_POINTER: + return cJSON_IsString(instance); + + /* Abstract, choice, and tuple types */ + case JS_TYPE_ABSTRACT: + case JS_TYPE_CHOICE: + return cJSON_IsObject(instance); + + case JS_TYPE_TUPLE: + return cJSON_IsArray(instance); + + case JS_TYPE_UNKNOWN: + default: + return false; + } +} + +/* Check if integer value is within the range for the given integer type */ +static bool check_integer_range(validate_context_t* ctx, double value, const char* type_name) { + /* Ensure it's an integer first */ + if (value != floor(value)) { + return false; + } + + int64_t int_val = (int64_t)value; + js_type_t type = js_type_from_name(type_name); + + switch (type) { + case JS_TYPE_INT8: + if (int_val < -128 || int_val > 127) { + char msg[128]; + snprintf(msg, sizeof(msg), "Value %lld is out of int8 range [-128, 127]", (long long)int_val); + add_error(ctx, JS_INSTANCE_INTEGER_OUT_OF_RANGE, msg); + return false; + } + break; + + case JS_TYPE_INT16: + if (int_val < -32768 || int_val > 32767) { + char msg[128]; + snprintf(msg, sizeof(msg), "Value %lld is out of int16 range [-32768, 32767]", (long long)int_val); + add_error(ctx, JS_INSTANCE_INTEGER_OUT_OF_RANGE, msg); + return false; + } + break; + + case JS_TYPE_INT32: + case JS_TYPE_INTEGER: + if (int_val < INT32_MIN || int_val > INT32_MAX) { + char msg[128]; + snprintf(msg, sizeof(msg), "Value %lld is out of int32 range", (long long)int_val); + add_error(ctx, JS_INSTANCE_INTEGER_OUT_OF_RANGE, msg); + return false; + } + break; + + case JS_TYPE_UINT8: + if (int_val < 0 || int_val > 255) { + char msg[128]; + snprintf(msg, sizeof(msg), "Value %lld is out of uint8 range [0, 255]", (long long)int_val); + add_error(ctx, JS_INSTANCE_INTEGER_OUT_OF_RANGE, msg); + return false; + } + break; + + case JS_TYPE_UINT16: + if (int_val < 0 || int_val > 65535) { + char msg[128]; + snprintf(msg, sizeof(msg), "Value %lld is out of uint16 range [0, 65535]", (long long)int_val); + add_error(ctx, JS_INSTANCE_INTEGER_OUT_OF_RANGE, msg); + return false; + } + break; + + case JS_TYPE_UINT32: + if (int_val < 0 || (uint64_t)int_val > UINT32_MAX) { + char msg[128]; + snprintf(msg, sizeof(msg), "Value %lld is out of uint32 range [0, 4294967295]", (long long)int_val); + add_error(ctx, JS_INSTANCE_INTEGER_OUT_OF_RANGE, msg); + return false; + } + break; + + default: + /* int64, uint64 - no range check needed as they match the native type */ + break; + } + + return true; +} + +/* ============================================================================ + * UTF-8 Support + * + * JSON strings are UTF-8 encoded. The minLength/maxLength constraints should + * count Unicode code points, not bytes. This matches JSON Schema behavior. + * ============================================================================ */ + +/** + * @brief Count Unicode code points in a UTF-8 string + * + * This counts code points (not grapheme clusters), which matches JSON Schema + * and most programming languages' string length semantics. + * + * @param str UTF-8 encoded string + * @return Number of Unicode code points + */ +static size_t utf8_codepoint_count(const char* str) { + size_t count = 0; + const unsigned char* s = (const unsigned char*)str; + + while (*s) { + /* Count only start bytes, skip continuation bytes (10xxxxxx) */ + if ((*s & 0xC0) != 0x80) { + count++; + } + s++; + } + + return count; +} + +/* ============================================================================ + * Constraint Validation + * ============================================================================ */ + +static bool validate_string_constraints(validate_context_t* ctx, const cJSON* instance, + const cJSON* schema) { + const char* str = instance->valuestring; + bool valid = true; + + const cJSON* minLength = cJSON_GetObjectItemCaseSensitive(schema, "minLength"); + const cJSON* maxLength = cJSON_GetObjectItemCaseSensitive(schema, "maxLength"); + const cJSON* pattern = cJSON_GetObjectItemCaseSensitive(schema, "pattern"); + + /* Use UTF-8 code point count for length constraints (matches JSON Schema) */ + size_t codepoint_len = 0; + if (minLength || maxLength) { + codepoint_len = utf8_codepoint_count(str); + } + + if (minLength && cJSON_IsNumber(minLength)) { + if (codepoint_len < (size_t)minLength->valuedouble) { + char msg[128]; + snprintf(msg, sizeof(msg), "String too short (min %d, got %zu)", + (int)minLength->valuedouble, codepoint_len); + add_error(ctx, JS_INSTANCE_STRING_TOO_SHORT, msg); + valid = false; + } + } + + if (maxLength && cJSON_IsNumber(maxLength)) { + if (codepoint_len > (size_t)maxLength->valuedouble) { + char msg[128]; + snprintf(msg, sizeof(msg), "String too long (max %d, got %zu)", + (int)maxLength->valuedouble, codepoint_len); + add_error(ctx, JS_INSTANCE_STRING_TOO_LONG, msg); + valid = false; + } + } + + if (pattern && cJSON_IsString(pattern)) { + if (!js_regex_match(pattern->valuestring, str)) { + char msg[256]; + snprintf(msg, sizeof(msg), "String '%s' does not match pattern '%s'", + str, pattern->valuestring); + add_error(ctx, JS_INSTANCE_STRING_PATTERN_MISMATCH, msg); + valid = false; + } + } + + return valid; +} + +static bool validate_numeric_constraints(validate_context_t* ctx, const cJSON* instance, + const cJSON* schema) { + double value = instance->valuedouble; + bool valid = true; + + const cJSON* minimum = cJSON_GetObjectItemCaseSensitive(schema, "minimum"); + const cJSON* maximum = cJSON_GetObjectItemCaseSensitive(schema, "maximum"); + const cJSON* exclusiveMinimum = cJSON_GetObjectItemCaseSensitive(schema, "exclusiveMinimum"); + const cJSON* exclusiveMaximum = cJSON_GetObjectItemCaseSensitive(schema, "exclusiveMaximum"); + const cJSON* multipleOf = cJSON_GetObjectItemCaseSensitive(schema, "multipleOf"); + + if (minimum && cJSON_IsNumber(minimum)) { + if (value < minimum->valuedouble) { + char msg[128]; + snprintf(msg, sizeof(msg), "Value %g is less than minimum %g", + value, minimum->valuedouble); + add_error(ctx, JS_INSTANCE_NUMBER_TOO_SMALL, msg); + valid = false; + } + } + + if (maximum && cJSON_IsNumber(maximum)) { + if (value > maximum->valuedouble) { + char msg[128]; + snprintf(msg, sizeof(msg), "Value %g is greater than maximum %g", + value, maximum->valuedouble); + add_error(ctx, JS_INSTANCE_NUMBER_TOO_LARGE, msg); + valid = false; + } + } + + if (exclusiveMinimum && cJSON_IsNumber(exclusiveMinimum)) { + if (value <= exclusiveMinimum->valuedouble) { + char msg[128]; + snprintf(msg, sizeof(msg), "Value %g must be greater than %g", + value, exclusiveMinimum->valuedouble); + add_error(ctx, JS_INSTANCE_NUMBER_TOO_SMALL, msg); + valid = false; + } + } + + if (exclusiveMaximum && cJSON_IsNumber(exclusiveMaximum)) { + if (value >= exclusiveMaximum->valuedouble) { + char msg[128]; + snprintf(msg, sizeof(msg), "Value %g must be less than %g", + value, exclusiveMaximum->valuedouble); + add_error(ctx, JS_INSTANCE_NUMBER_TOO_LARGE, msg); + valid = false; + } + } + + if (multipleOf && cJSON_IsNumber(multipleOf) && multipleOf->valuedouble != 0) { + double quotient = value / multipleOf->valuedouble; + if (fabs(quotient - round(quotient)) > 1e-10) { + char msg[128]; + snprintf(msg, sizeof(msg), "Value %g is not a multiple of %g", + value, multipleOf->valuedouble); + add_error(ctx, JS_INSTANCE_NUMBER_NOT_MULTIPLE, msg); + valid = false; + } + } + + return valid; +} + +static bool validate_array_constraints(validate_context_t* ctx, const cJSON* instance, + const cJSON* schema) { + int size = cJSON_GetArraySize(instance); + bool valid = true; + + const cJSON* minItems = cJSON_GetObjectItemCaseSensitive(schema, "minItems"); + const cJSON* maxItems = cJSON_GetObjectItemCaseSensitive(schema, "maxItems"); + const cJSON* uniqueItems = cJSON_GetObjectItemCaseSensitive(schema, "uniqueItems"); + const cJSON* contains = cJSON_GetObjectItemCaseSensitive(schema, "contains"); + const cJSON* minContains = cJSON_GetObjectItemCaseSensitive(schema, "minContains"); + const cJSON* maxContains = cJSON_GetObjectItemCaseSensitive(schema, "maxContains"); + + if (minItems && cJSON_IsNumber(minItems)) { + if (size < (int)minItems->valuedouble) { + char msg[128]; + snprintf(msg, sizeof(msg), "Array too short (min %d, got %d)", + (int)minItems->valuedouble, size); + add_error(ctx, JS_INSTANCE_ARRAY_TOO_SHORT, msg); + valid = false; + } + } + + if (maxItems && cJSON_IsNumber(maxItems)) { + if (size > (int)maxItems->valuedouble) { + char msg[128]; + snprintf(msg, sizeof(msg), "Array too long (max %d, got %d)", + (int)maxItems->valuedouble, size); + add_error(ctx, JS_INSTANCE_ARRAY_TOO_LONG, msg); + valid = false; + } + } + + if (uniqueItems && cJSON_IsTrue(uniqueItems)) { + /* Use optimized O(n) hash-based check for primitive arrays */ + if (is_primitive_array(instance)) { + int dup_i = -1, dup_j = -1; + if (!check_unique_primitive(instance, &dup_i, &dup_j)) { + char msg[128]; + snprintf(msg, sizeof(msg), "Array items at indices %d and %d are not unique", + dup_i, dup_j); + add_error(ctx, JS_INSTANCE_ARRAY_NOT_UNIQUE, msg); + valid = false; + } + } else { + /* Fall back to O(n²) for arrays with objects/arrays */ + for (int i = 0; i < size && valid; i++) { + cJSON* item_i = cJSON_GetArrayItem(instance, i); + for (int j = i + 1; j < size; j++) { + cJSON* item_j = cJSON_GetArrayItem(instance, j); + if (cJSON_Compare(item_i, item_j, true)) { + char msg[128]; + snprintf(msg, sizeof(msg), "Array items at indices %d and %d are not unique", + i, j); + add_error(ctx, JS_INSTANCE_ARRAY_NOT_UNIQUE, msg); + valid = false; + break; + } + } + } + } + } + + /* Validate contains constraint */ + if (contains && cJSON_IsObject(contains)) { + int contains_count = 0; + int min_contains_val = minContains && cJSON_IsNumber(minContains) ? + (int)minContains->valuedouble : 1; + int max_contains_val = maxContains && cJSON_IsNumber(maxContains) ? + (int)maxContains->valuedouble : size + 1; + + /* Count items matching the contains schema */ + for (int i = 0; i < size; i++) { + cJSON* item = cJSON_GetArrayItem(instance, i); + size_t prev_errors = ctx->result->error_count; + + /* Try to validate item against contains schema */ + size_t prev_path_len = strlen(ctx->path); + char index_str[32]; + snprintf(index_str, sizeof(index_str), "[%d]", i); + push_path(ctx, index_str); + + if (validate_instance(ctx, item, contains)) { + contains_count++; + } else { + /* Remove errors from failed contains check - these are expected */ + /* Clean up the error strings before reducing count */ + for (size_t e = prev_errors; e < ctx->result->error_count; e++) { + js_error_cleanup(&ctx->result->errors[e]); + } + ctx->result->error_count = prev_errors; + } + + pop_path(ctx, prev_path_len); + } + + if (contains_count < min_contains_val) { + char msg[128]; + snprintf(msg, sizeof(msg), "Array does not contain enough matching items (min %d, got %d)", + min_contains_val, contains_count); + add_error(ctx, JS_INSTANCE_ARRAY_CONTAINS_TOO_FEW, msg); + valid = false; + } + + if (contains_count > max_contains_val) { + char msg[128]; + snprintf(msg, sizeof(msg), "Array contains too many matching items (max %d, got %d)", + max_contains_val, contains_count); + add_error(ctx, JS_INSTANCE_ARRAY_CONTAINS_TOO_MANY, msg); + valid = false; + } + } + + return valid; +} + +static bool validate_object_constraints(validate_context_t* ctx, const cJSON* instance, + const cJSON* schema) { + int size = cJSON_GetArraySize(instance); + bool valid = true; + + const cJSON* minProperties = cJSON_GetObjectItemCaseSensitive(schema, "minProperties"); + const cJSON* maxProperties = cJSON_GetObjectItemCaseSensitive(schema, "maxProperties"); + + if (minProperties && cJSON_IsNumber(minProperties)) { + if (size < (int)minProperties->valuedouble) { + char msg[128]; + snprintf(msg, sizeof(msg), "Too few properties (min %d, got %d)", + (int)minProperties->valuedouble, size); + add_error(ctx, JS_INSTANCE_TOO_FEW_PROPERTIES, msg); + valid = false; + } + } + + if (maxProperties && cJSON_IsNumber(maxProperties)) { + if (size > (int)maxProperties->valuedouble) { + char msg[128]; + snprintf(msg, sizeof(msg), "Too many properties (max %d, got %d)", + (int)maxProperties->valuedouble, size); + add_error(ctx, JS_INSTANCE_TOO_MANY_PROPERTIES, msg); + valid = false; + } + } + + return valid; +} + +/* ============================================================================ + * Enum/Const Validation + * ============================================================================ */ + +static bool validate_enum(validate_context_t* ctx, const cJSON* instance, const cJSON* enum_arr) { + cJSON* item; + cJSON_ArrayForEach(item, enum_arr) { + if (cJSON_Compare(instance, item, true)) { + return true; + } + } + add_error(ctx, JS_INSTANCE_ENUM_MISMATCH, "Value not in enum"); + return false; +} + +static bool validate_const(validate_context_t* ctx, const cJSON* instance, const cJSON* const_val) { + if (cJSON_Compare(instance, const_val, true)) { + return true; + } + add_error(ctx, JS_INSTANCE_CONST_MISMATCH, "Value does not match const"); + return false; +} + +/* ============================================================================ + * Type-Specific Validation + * ============================================================================ */ + +static bool validate_object_instance(validate_context_t* ctx, const cJSON* instance, + const cJSON* schema) { + bool valid = true; + + const cJSON* properties = cJSON_GetObjectItemCaseSensitive(schema, "properties"); + const cJSON* required = cJSON_GetObjectItemCaseSensitive(schema, "required"); + const cJSON* additionalProperties = cJSON_GetObjectItemCaseSensitive(schema, "additionalProperties"); + const cJSON* dependentRequired = cJSON_GetObjectItemCaseSensitive(schema, "dependentRequired"); + + /* Check required properties */ + if (required && cJSON_IsArray(required)) { + cJSON* req_item; + cJSON_ArrayForEach(req_item, required) { + if (cJSON_IsString(req_item)) { + const cJSON* prop = cJSON_GetObjectItemCaseSensitive(instance, req_item->valuestring); + if (!prop) { + char msg[256]; + snprintf(msg, sizeof(msg), "Required property '%s' is missing", + req_item->valuestring); + add_error(ctx, JS_INSTANCE_REQUIRED_MISSING, msg); + valid = false; + } + } + } + } + + /* Check dependentRequired - if property X is present, properties Y, Z, ... must also be present */ + if (dependentRequired && cJSON_IsObject(dependentRequired)) { + cJSON* dep; + cJSON_ArrayForEach(dep, dependentRequired) { + const char* trigger_prop = dep->string; + /* Only check if the trigger property is present in the instance */ + const cJSON* trigger = cJSON_GetObjectItemCaseSensitive(instance, trigger_prop); + if (trigger && cJSON_IsArray(dep)) { + /* Check that all required dependent properties are present */ + cJSON* req_prop; + cJSON_ArrayForEach(req_prop, dep) { + if (cJSON_IsString(req_prop)) { + const cJSON* prop = cJSON_GetObjectItemCaseSensitive(instance, req_prop->valuestring); + if (!prop) { + char msg[256]; + snprintf(msg, sizeof(msg), + "Property '%s' requires property '%s' to also be present", + trigger_prop, req_prop->valuestring); + add_error(ctx, JS_INSTANCE_DEPENDENT_REQUIRED_MISSING, msg); + valid = false; + } + } + } + } + } + } + + /* Validate each property */ + cJSON* prop; + cJSON_ArrayForEach(prop, instance) { + const char* prop_name = prop->string; + const cJSON* prop_schema = NULL; + + if (properties && cJSON_IsObject(properties)) { + prop_schema = cJSON_GetObjectItemCaseSensitive(properties, prop_name); + } + + if (prop_schema) { + size_t prev_len = strlen(ctx->path); + push_path(ctx, prop_name); + if (!validate_instance(ctx, prop, prop_schema)) { + valid = false; + } + pop_path(ctx, prev_len); + } else if (additionalProperties) { + /* Schema explicitly controls additional properties */ + if (cJSON_IsFalse(additionalProperties)) { + /* additionalProperties: false - reject additional properties */ + char msg[256]; + snprintf(msg, sizeof(msg), "Additional property '%s' not allowed", prop_name); + add_error(ctx, JS_INSTANCE_ADDITIONAL_PROPERTY, msg); + valid = false; + } else if (cJSON_IsObject(additionalProperties)) { + size_t prev_len = strlen(ctx->path); + push_path(ctx, prop_name); + if (!validate_instance(ctx, prop, additionalProperties)) { + valid = false; + } + pop_path(ctx, prev_len); + } + /* additionalProperties: true or missing - allow */ + } else if (!ctx->validator->options.allow_additional_properties) { + /* No additionalProperties in schema, use validator option */ + char msg[256]; + snprintf(msg, sizeof(msg), "Additional property '%s' not allowed", prop_name); + add_error(ctx, JS_INSTANCE_ADDITIONAL_PROPERTY, msg); + valid = false; + } + } + + /* Validate object constraints */ + if (!validate_object_constraints(ctx, instance, schema)) { + valid = false; + } + + return valid; +} + +static bool validate_array_instance(validate_context_t* ctx, const cJSON* instance, + const cJSON* schema) { + bool valid = true; + + const cJSON* items = cJSON_GetObjectItemCaseSensitive(schema, "items"); + + /* Validate each item */ + if (items) { + int idx = 0; + cJSON* item; + cJSON_ArrayForEach(item, instance) { + char idx_str[32]; + snprintf(idx_str, sizeof(idx_str), "[%d]", idx); + size_t prev_len = strlen(ctx->path); + push_path(ctx, idx_str); + + if (!validate_instance(ctx, item, items)) { + valid = false; + } + + pop_path(ctx, prev_len); + idx++; + } + } + + /* Validate array constraints */ + if (!validate_array_constraints(ctx, instance, schema)) { + valid = false; + } + + return valid; +} + +static bool validate_set_instance(validate_context_t* ctx, const cJSON* instance, + const cJSON* schema) { + bool valid = true; + + const cJSON* items = cJSON_GetObjectItemCaseSensitive(schema, "items"); + + /* Validate each item */ + if (items) { + int idx = 0; + cJSON* item; + cJSON_ArrayForEach(item, instance) { + char idx_str[32]; + snprintf(idx_str, sizeof(idx_str), "[%d]", idx); + size_t prev_len = strlen(ctx->path); + push_path(ctx, idx_str); + + if (!validate_instance(ctx, item, items)) { + valid = false; + } + + pop_path(ctx, prev_len); + idx++; + } + } + + /* Sets must have unique items - always check regardless of uniqueItems keyword */ + int size = cJSON_GetArraySize(instance); + for (int i = 0; i < size; i++) { + cJSON* item_i = cJSON_GetArrayItem(instance, i); + for (int j = i + 1; j < size; j++) { + cJSON* item_j = cJSON_GetArrayItem(instance, j); + if (cJSON_Compare(item_i, item_j, true)) { + char msg[128]; + snprintf(msg, sizeof(msg), "Set items at indices %d and %d are not unique", + i, j); + add_error(ctx, JS_INSTANCE_SET_NOT_UNIQUE, msg); + valid = false; + break; + } + } + } + + /* Validate other array constraints (minItems, maxItems) */ + const cJSON* minItems = cJSON_GetObjectItemCaseSensitive(schema, "minItems"); + const cJSON* maxItems = cJSON_GetObjectItemCaseSensitive(schema, "maxItems"); + + if (minItems && cJSON_IsNumber(minItems)) { + if (size < (int)minItems->valuedouble) { + char msg[128]; + snprintf(msg, sizeof(msg), "Set too small (min %d, got %d)", + (int)minItems->valuedouble, size); + add_error(ctx, JS_INSTANCE_ARRAY_TOO_SHORT, msg); + valid = false; + } + } + + if (maxItems && cJSON_IsNumber(maxItems)) { + if (size > (int)maxItems->valuedouble) { + char msg[128]; + snprintf(msg, sizeof(msg), "Set too large (max %d, got %d)", + (int)maxItems->valuedouble, size); + add_error(ctx, JS_INSTANCE_ARRAY_TOO_LONG, msg); + valid = false; + } + } + + return valid; +} + +static bool validate_map_instance(validate_context_t* ctx, const cJSON* instance, + const cJSON* schema) { + bool valid = true; + + const cJSON* values = cJSON_GetObjectItemCaseSensitive(schema, "values"); + const cJSON* keys = cJSON_GetObjectItemCaseSensitive(schema, "keys"); + + cJSON* entry; + cJSON_ArrayForEach(entry, instance) { + const char* key = entry->string; + + /* Validate key pattern if specified */ + if (keys && cJSON_IsObject(keys)) { + const cJSON* key_pattern = cJSON_GetObjectItemCaseSensitive(keys, "pattern"); + if (key_pattern && cJSON_IsString(key_pattern)) { + if (!js_regex_match(key_pattern->valuestring, key)) { + char msg[256]; + snprintf(msg, sizeof(msg), "Map key '%s' does not match pattern '%s'", + key, key_pattern->valuestring); + add_error(ctx, JS_INSTANCE_MAP_KEY_PATTERN_MISMATCH, msg); + valid = false; + } + } + } + + /* Validate value */ + if (values) { + size_t prev_len = strlen(ctx->path); + char key_path[PATH_BUFFER_SIZE]; + snprintf(key_path, sizeof(key_path), "[%s]", key); + push_path(ctx, key_path); + + if (!validate_instance(ctx, entry, values)) { + valid = false; + } + + pop_path(ctx, prev_len); + } + } + + /* Validate map size constraints */ + int size = cJSON_GetArraySize(instance); + const cJSON* minEntries = cJSON_GetObjectItemCaseSensitive(schema, "minEntries"); + const cJSON* maxEntries = cJSON_GetObjectItemCaseSensitive(schema, "maxEntries"); + + /* Fall back to minProperties/maxProperties for compatibility */ + if (!minEntries) minEntries = cJSON_GetObjectItemCaseSensitive(schema, "minProperties"); + if (!maxEntries) maxEntries = cJSON_GetObjectItemCaseSensitive(schema, "maxProperties"); + + if (minEntries && cJSON_IsNumber(minEntries)) { + if (size < (int)minEntries->valuedouble) { + char msg[128]; + snprintf(msg, sizeof(msg), "Map has too few entries (min %d, got %d)", + (int)minEntries->valuedouble, size); + add_error(ctx, JS_INSTANCE_MAP_TOO_FEW_ENTRIES, msg); + valid = false; + } + } + + if (maxEntries && cJSON_IsNumber(maxEntries)) { + if (size > (int)maxEntries->valuedouble) { + char msg[128]; + snprintf(msg, sizeof(msg), "Map has too many entries (max %d, got %d)", + (int)maxEntries->valuedouble, size); + add_error(ctx, JS_INSTANCE_MAP_TOO_MANY_ENTRIES, msg); + valid = false; + } + } + + /* Validate keyNames if specified */ + const cJSON* keyNames = cJSON_GetObjectItemCaseSensitive(schema, "keyNames"); + if (keyNames && cJSON_IsObject(keyNames)) { + const cJSON* key_pattern = cJSON_GetObjectItemCaseSensitive(keyNames, "pattern"); + const cJSON* minLength = cJSON_GetObjectItemCaseSensitive(keyNames, "minLength"); + const cJSON* maxLength = cJSON_GetObjectItemCaseSensitive(keyNames, "maxLength"); + + cJSON* entry2; + cJSON_ArrayForEach(entry2, instance) { + const char* key = entry2->string; + size_t key_len = strlen(key); + + if (minLength && cJSON_IsNumber(minLength)) { + if (key_len < (size_t)minLength->valuedouble) { + char msg[256]; + snprintf(msg, sizeof(msg), "Map key '%s' too short (min %d)", + key, (int)minLength->valuedouble); + add_error(ctx, JS_INSTANCE_MAP_KEY_PATTERN_MISMATCH, msg); + valid = false; + } + } + + if (maxLength && cJSON_IsNumber(maxLength)) { + if (key_len > (size_t)maxLength->valuedouble) { + char msg[256]; + snprintf(msg, sizeof(msg), "Map key '%s' too long (max %d)", + key, (int)maxLength->valuedouble); + add_error(ctx, JS_INSTANCE_MAP_KEY_PATTERN_MISMATCH, msg); + valid = false; + } + } + + if (key_pattern && cJSON_IsString(key_pattern)) { + if (!js_regex_match(key_pattern->valuestring, key)) { + char msg[256]; + snprintf(msg, sizeof(msg), "Map key '%s' does not match pattern '%s'", + key, key_pattern->valuestring); + add_error(ctx, JS_INSTANCE_MAP_KEY_PATTERN_MISMATCH, msg); + valid = false; + } + } + } + } + + return valid; +} + +static bool validate_choice_instance(validate_context_t* ctx, const cJSON* instance, + const cJSON* schema) { + const cJSON* choices = cJSON_GetObjectItemCaseSensitive(schema, "choices"); + const cJSON* selector = cJSON_GetObjectItemCaseSensitive(schema, "selector"); + + if (!choices || !cJSON_IsObject(choices)) { + add_error(ctx, JS_INSTANCE_CHOICE_NO_MATCH, "Schema missing choices"); + return false; + } + + /* If selector is specified, use it to determine the choice */ + if (selector && cJSON_IsString(selector)) { + const cJSON* selector_value = cJSON_GetObjectItemCaseSensitive(instance, selector->valuestring); + if (!selector_value) { + add_error(ctx, JS_INSTANCE_CHOICE_SELECTOR_MISSING, "Choice selector property missing"); + return false; + } + if (!cJSON_IsString(selector_value)) { + add_error(ctx, JS_INSTANCE_CHOICE_SELECTOR_INVALID, "Choice selector must be a string"); + return false; + } + + const cJSON* choice_schema = cJSON_GetObjectItemCaseSensitive(choices, selector_value->valuestring); + if (!choice_schema) { + char msg[256]; + snprintf(msg, sizeof(msg), "Unknown choice: '%s'", selector_value->valuestring); + add_error(ctx, JS_INSTANCE_CHOICE_UNKNOWN, msg); + return false; + } + + return validate_instance(ctx, instance, choice_schema); + } + + /* Without selector, try each choice and find a match */ + int match_count = 0; + const cJSON* matched_choice = NULL; + + cJSON* choice; + cJSON_ArrayForEach(choice, choices) { + js_result_t temp_result; + js_result_init(&temp_result); + + validate_context_t temp_ctx = *ctx; + temp_ctx.result = &temp_result; + + if (validate_instance(&temp_ctx, instance, choice)) { + match_count++; + matched_choice = choice; + } + + js_result_cleanup(&temp_result); + } + + if (match_count == 0) { + add_error(ctx, JS_INSTANCE_CHOICE_NO_MATCH, "No matching choice"); + return false; + } + + if (match_count > 1) { + add_error(ctx, JS_INSTANCE_CHOICE_MULTIPLE_MATCHES, "Multiple choices matched"); + return false; + } + + /* Re-validate against the matched choice to populate errors if any */ + return validate_instance(ctx, instance, matched_choice); +} + +/* ============================================================================ + * Composition Validation + * ============================================================================ */ + +static bool validate_allOf(validate_context_t* ctx, const cJSON* instance, const cJSON* allOf) { + bool valid = true; + int idx = 0; + cJSON* subschema; + cJSON_ArrayForEach(subschema, allOf) { + char idx_str[32]; + snprintf(idx_str, sizeof(idx_str), "allOf[%d]", idx); + size_t prev_len = strlen(ctx->path); + push_path(ctx, idx_str); + + if (!validate_instance(ctx, instance, subschema)) { + valid = false; + } + + pop_path(ctx, prev_len); + idx++; + } + return valid; +} + +static bool validate_anyOf(validate_context_t* ctx, const cJSON* instance, const cJSON* anyOf) { + cJSON* subschema; + cJSON_ArrayForEach(subschema, anyOf) { + js_result_t temp_result; + js_result_init(&temp_result); + + validate_context_t temp_ctx = *ctx; + temp_ctx.result = &temp_result; + + if (validate_instance(&temp_ctx, instance, subschema)) { + js_result_cleanup(&temp_result); + return true; + } + + js_result_cleanup(&temp_result); + } + + add_error(ctx, JS_INSTANCE_ANYOF_FAILED, "No schema in anyOf matched"); + return false; +} + +static bool validate_oneOf(validate_context_t* ctx, const cJSON* instance, const cJSON* oneOf) { + int match_count = 0; + + cJSON* subschema; + cJSON_ArrayForEach(subschema, oneOf) { + js_result_t temp_result; + js_result_init(&temp_result); + + validate_context_t temp_ctx = *ctx; + temp_ctx.result = &temp_result; + + if (validate_instance(&temp_ctx, instance, subschema)) { + match_count++; + } + + js_result_cleanup(&temp_result); + } + + if (match_count == 0) { + add_error(ctx, JS_INSTANCE_ONEOF_FAILED, "No schema in oneOf matched"); + return false; + } + + if (match_count > 1) { + add_error(ctx, JS_INSTANCE_ONEOF_MULTIPLE, "Multiple schemas in oneOf matched"); + return false; + } + + return true; +} + +static bool validate_not(validate_context_t* ctx, const cJSON* instance, const cJSON* not_schema) { + js_result_t temp_result; + js_result_init(&temp_result); + + validate_context_t temp_ctx = *ctx; + temp_ctx.result = &temp_result; + + bool matches = validate_instance(&temp_ctx, instance, not_schema); + js_result_cleanup(&temp_result); + + if (matches) { + add_error(ctx, JS_INSTANCE_NOT_FAILED, "Value matches schema in 'not'"); + return false; + } + + return true; +} + +static bool validate_if_then_else(validate_context_t* ctx, const cJSON* instance, + const cJSON* if_schema, const cJSON* then_schema, + const cJSON* else_schema) { + js_result_t temp_result; + js_result_init(&temp_result); + + validate_context_t temp_ctx = *ctx; + temp_ctx.result = &temp_result; + + bool if_matches = validate_instance(&temp_ctx, instance, if_schema); + js_result_cleanup(&temp_result); + + if (if_matches) { + if (then_schema) { + return validate_instance(ctx, instance, then_schema); + } + } else { + if (else_schema) { + return validate_instance(ctx, instance, else_schema); + } + } + + return true; +} + +/* ============================================================================ + * Main Validation Logic + * ============================================================================ */ + +static bool validate_instance(validate_context_t* ctx, const cJSON* instance, const cJSON* schema) { + if (!schema || !cJSON_IsObject(schema)) { + return true; /* Empty or invalid schema matches anything */ + } + + ctx->depth++; + if (ctx->depth > MAX_DEPTH) { + add_error(ctx, JS_SCHEMA_MAX_DEPTH_EXCEEDED, "Maximum validation depth exceeded"); + ctx->depth--; + return false; + } + + bool valid = true; + + /* Handle $ref */ + const cJSON* ref = cJSON_GetObjectItemCaseSensitive(schema, "$ref"); + if (ref && cJSON_IsString(ref)) { + const cJSON* resolved = resolve_ref(ctx, ref->valuestring); + if (!resolved) { + char msg[256]; + snprintf(msg, sizeof(msg), "Cannot resolve reference: %s", ref->valuestring); + add_error(ctx, JS_INSTANCE_REF_NOT_FOUND, msg); + ctx->depth--; + return false; + } + bool result = validate_instance(ctx, instance, resolved); + ctx->depth--; + return result; + } + + /* Check type */ + const cJSON* type_node = cJSON_GetObjectItemCaseSensitive(schema, "type"); + if (type_node) { + /* Handle type as object with $ref (JSON Structure pattern) */ + if (cJSON_IsObject(type_node)) { + const cJSON* type_ref = cJSON_GetObjectItemCaseSensitive(type_node, "$ref"); + if (type_ref && cJSON_IsString(type_ref)) { + const cJSON* resolved = resolve_ref(ctx, type_ref->valuestring); + if (!resolved) { + char msg[256]; + snprintf(msg, sizeof(msg), "Cannot resolve type reference: %s", type_ref->valuestring); + add_error(ctx, JS_INSTANCE_REF_NOT_FOUND, msg); + ctx->depth--; + return false; + } + bool result = validate_instance(ctx, instance, resolved); + ctx->depth--; + return result; + } + } else if (cJSON_IsString(type_node)) { + const char* type_str = type_node->valuestring; + if (!check_type_match(instance, type_str)) { + char msg[128]; + snprintf(msg, sizeof(msg), "Expected type '%s'", type_str); + add_error(ctx, JS_INSTANCE_TYPE_MISMATCH, msg); + valid = false; + } else { + /* Type-specific validation and constraints */ + if (strcmp(type_str, "object") == 0) { + if (!validate_object_instance(ctx, instance, schema)) valid = false; + } else if (strcmp(type_str, "array") == 0) { + if (!validate_array_instance(ctx, instance, schema)) valid = false; + } else if (strcmp(type_str, "set") == 0) { + if (!validate_set_instance(ctx, instance, schema)) valid = false; + } else if (strcmp(type_str, "map") == 0) { + if (!validate_map_instance(ctx, instance, schema)) valid = false; + } else if (strcmp(type_str, "choice") == 0) { + if (!validate_choice_instance(ctx, instance, schema)) valid = false; + } else if (strcmp(type_str, "string") == 0 || js_type_is_string(js_type_from_name(type_str))) { + if (cJSON_IsString(instance)) { + if (!validate_string_constraints(ctx, instance, schema)) valid = false; + } + } else if (js_type_is_numeric(js_type_from_name(type_str))) { + if (cJSON_IsNumber(instance)) { + if (!validate_numeric_constraints(ctx, instance, schema)) valid = false; + /* Check integer range for sized types */ + if (js_type_is_integer(js_type_from_name(type_str))) { + if (!check_integer_range(ctx, instance->valuedouble, type_str)) { + valid = false; + } + } + } + } + } + } else if (cJSON_IsArray(type_node)) { + /* Union types - check if instance matches any type */ + bool type_matched = false; + cJSON* type_item; + cJSON_ArrayForEach(type_item, type_node) { + if (cJSON_IsString(type_item) && check_type_match(instance, type_item->valuestring)) { + type_matched = true; + break; + } + } + if (!type_matched) { + add_error(ctx, JS_INSTANCE_UNION_NO_MATCH, "Value does not match any type in union"); + valid = false; + } + } + } + + /* Validate enum */ + const cJSON* enum_node = cJSON_GetObjectItemCaseSensitive(schema, "enum"); + if (enum_node && cJSON_IsArray(enum_node)) { + if (!validate_enum(ctx, instance, enum_node)) valid = false; + } + + /* Validate const */ + const cJSON* const_node = cJSON_GetObjectItemCaseSensitive(schema, "const"); + if (const_node) { + if (!validate_const(ctx, instance, const_node)) valid = false; + } + + /* Composition keywords */ + const cJSON* allOf = cJSON_GetObjectItemCaseSensitive(schema, "allOf"); + const cJSON* anyOf = cJSON_GetObjectItemCaseSensitive(schema, "anyOf"); + const cJSON* oneOf = cJSON_GetObjectItemCaseSensitive(schema, "oneOf"); + const cJSON* not_schema = cJSON_GetObjectItemCaseSensitive(schema, "not"); + const cJSON* if_schema = cJSON_GetObjectItemCaseSensitive(schema, "if"); + const cJSON* then_schema = cJSON_GetObjectItemCaseSensitive(schema, "then"); + const cJSON* else_schema = cJSON_GetObjectItemCaseSensitive(schema, "else"); + + if (allOf && cJSON_IsArray(allOf)) { + if (!validate_allOf(ctx, instance, allOf)) valid = false; + } + + if (anyOf && cJSON_IsArray(anyOf)) { + if (!validate_anyOf(ctx, instance, anyOf)) valid = false; + } + + if (oneOf && cJSON_IsArray(oneOf)) { + if (!validate_oneOf(ctx, instance, oneOf)) valid = false; + } + + if (not_schema) { + if (!validate_not(ctx, instance, not_schema)) valid = false; + } + + if (if_schema) { + if (!validate_if_then_else(ctx, instance, if_schema, then_schema, else_schema)) valid = false; + } + + ctx->depth--; + return valid; +} + +/* ============================================================================ + * Public API + * ============================================================================ */ + +void js_instance_validator_init(js_instance_validator_t* validator) { + if (validator) { + validator->options = JS_INSTANCE_OPTIONS_DEFAULT; + } +} + +void js_instance_validator_init_with_options(js_instance_validator_t* validator, + js_instance_options_t options) { + if (validator) { + validator->options = options; + } +} + +bool js_instance_validate(const js_instance_validator_t* validator, + const cJSON* instance, + const cJSON* schema, + js_result_t* result) { + if (!validator || !result) return false; + + js_result_init(result); + + if (!instance) { + js_result_add_error(result, JS_INSTANCE_TYPE_MISMATCH, "Instance is null", ""); + return false; + } + + if (!schema) { + js_result_add_error(result, JS_SCHEMA_NULL, "Schema is null", ""); + return false; + } + + /* Always process imports - either to resolve them (if allowed) or to detect errors */ + cJSON* schema_copy = NULL; + const cJSON* working_schema = schema; + + /* Create a mutable copy of the schema for import processing */ + schema_copy = cJSON_Duplicate(schema, true); + if (!schema_copy) { + js_result_add_error(result, JS_SCHEMA_NULL, "Failed to copy schema", ""); + return false; + } + + /* Process imports - will report error if found but not allowed */ + import_context_t import_ctx = { + .validator = validator, + .result = result, + .import_depth = 0 + }; + + if (!process_imports(&import_ctx, schema_copy, "#")) { + cJSON_Delete(schema_copy); + return false; + } + + working_schema = schema_copy; + + /* Cache definitions for reference resolution - primary keyword is "definitions" */ + const cJSON* defs = cJSON_GetObjectItemCaseSensitive(working_schema, "definitions"); + if (!defs) { + defs = cJSON_GetObjectItemCaseSensitive(working_schema, "$defs"); + } + if (!defs) { + defs = cJSON_GetObjectItemCaseSensitive(working_schema, "$definitions"); + } + + validate_context_t ctx = { + .validator = validator, + .result = result, + .root_schema = working_schema, + .definitions = defs, + .path = "", + .depth = 0 + }; + + bool valid = validate_instance(&ctx, instance, working_schema); + + if (schema_copy) { + cJSON_Delete(schema_copy); + } + + return valid; +} + +bool js_instance_validate_strings(const js_instance_validator_t* validator, + const char* instance_json, + const char* schema_json, + js_result_t* result) { + if (!validator || !result) return false; + + js_result_init(result); + + if (!instance_json) { + js_result_add_error(result, JS_INSTANCE_TYPE_MISMATCH, "Instance JSON is null", ""); + return false; + } + + if (!schema_json) { + js_result_add_error(result, JS_SCHEMA_NULL, "Schema JSON is null", ""); + return false; + } + + cJSON* instance = cJSON_Parse(instance_json); + if (!instance) { + const char* error_ptr = cJSON_GetErrorPtr(); + char msg[256]; + if (error_ptr) { + snprintf(msg, sizeof(msg), "Instance JSON parse error near: %.50s", error_ptr); + } else { + snprintf(msg, sizeof(msg), "Instance JSON parse error"); + } + js_result_add_error(result, JS_INSTANCE_TYPE_MISMATCH, msg, ""); + return false; + } + + cJSON* schema = cJSON_Parse(schema_json); + if (!schema) { + const char* error_ptr = cJSON_GetErrorPtr(); + char msg[256]; + if (error_ptr) { + snprintf(msg, sizeof(msg), "Schema JSON parse error near: %.50s", error_ptr); + } else { + snprintf(msg, sizeof(msg), "Schema JSON parse error"); + } + js_result_add_error(result, JS_SCHEMA_INVALID_TYPE, msg, ""); + cJSON_Delete(instance); + return false; + } + + bool valid = js_instance_validate(validator, instance, schema, result); + + cJSON_Delete(instance); + cJSON_Delete(schema); + + return valid; +} diff --git a/r/src/json_source_locator.c b/r/src/json_source_locator.c new file mode 100644 index 0000000..eb0faad --- /dev/null +++ b/r/src/json_source_locator.c @@ -0,0 +1,438 @@ +/** + * @file json_source_locator.c + * @brief JSON source location tracking + * + * Provides utilities for tracking source locations (line, column, offset) + * within JSON documents for better error reporting. + * + * Copyright (c) 2024 JSON Structure Contributors + * SPDX-License-Identifier: MIT + */ + +#include "json_structure/json_structure.h" +#include "json_structure/types.h" +#include +#include +#include + +/* ============================================================================ + * Source Location Tracking + * ============================================================================ */ + +/** + * @brief State for JSON source location tracking + */ +typedef struct js_source_locator { + const char* source; + size_t source_len; + size_t* line_offsets; + size_t line_count; + size_t line_capacity; +} js_source_locator_t; + +/** + * @brief Initialize a source locator + */ +static bool js_source_locator_init(js_source_locator_t* locator, const char* source) { + if (!locator || !source) return false; + + locator->source = source; + locator->source_len = strlen(source); + locator->line_count = 0; + locator->line_capacity = 64; + locator->line_offsets = (size_t*)js_malloc(locator->line_capacity * sizeof(size_t)); + + if (!locator->line_offsets) return false; + + /* First line starts at offset 0 */ + locator->line_offsets[0] = 0; + locator->line_count = 1; + + /* Find all line starts (limit to prevent resource exhaustion) */ + #define MAX_LINES 1000000 /* 1 million lines max */ + for (size_t i = 0; i < locator->source_len; ++i) { + if (source[i] == '\n') { + /* Next line starts after this newline */ + if (locator->line_count >= locator->line_capacity) { + /* Check for maximum line limit */ + if (locator->line_capacity >= MAX_LINES) { + /* Reached max - stop tracking but don't fail */ + break; + } + size_t new_capacity = locator->line_capacity * 2; + if (new_capacity > MAX_LINES) new_capacity = MAX_LINES; + /* Check for multiplication overflow */ + if (new_capacity > SIZE_MAX / sizeof(size_t)) { + js_free(locator->line_offsets); + locator->line_offsets = NULL; + return false; + } + size_t* new_offsets = (size_t*)js_realloc( + locator->line_offsets, new_capacity * sizeof(size_t)); + if (!new_offsets) { + js_free(locator->line_offsets); + locator->line_offsets = NULL; + return false; + } + locator->line_offsets = new_offsets; + locator->line_capacity = new_capacity; + } + locator->line_offsets[locator->line_count] = i + 1; + locator->line_count++; + } + } + #undef MAX_LINES + + return true; +} + +/** + * @brief Clean up a source locator + */ +static void js_source_locator_cleanup(js_source_locator_t* locator) { + if (locator) { + js_free(locator->line_offsets); + locator->line_offsets = NULL; + locator->line_count = 0; + locator->line_capacity = 0; + } +} + +/** + * @brief Get location for a given offset + */ +static js_location_t js_source_locator_get_location(const js_source_locator_t* locator, + size_t offset) { + js_location_t loc = {0, 0, offset}; + + if (!locator || !locator->line_offsets || offset > locator->source_len) { + return loc; + } + + /* Binary search for the line */ + size_t low = 0; + size_t high = locator->line_count; + + while (low < high) { + size_t mid = (low + high) / 2; + if (locator->line_offsets[mid] <= offset) { + low = mid + 1; + } else { + high = mid; + } + } + + /* low-1 is now the line index (0-based) */ + size_t line_idx = (low > 0) ? low - 1 : 0; + + loc.line = (int)(line_idx + 1); /* 1-based line number */ + loc.column = (int)(offset - locator->line_offsets[line_idx] + 1); /* 1-based column */ + + return loc; +} + +/* ============================================================================ + * JSON Path to Offset Mapping + * ============================================================================ */ + +/** + * @brief Skip whitespace in JSON source + */ +static size_t skip_whitespace(const char* s, size_t pos, size_t len) { + while (pos < len && isspace((unsigned char)s[pos])) { + pos++; + } + return pos; +} + +/** + * @brief Skip a JSON string (including quotes) + */ +static size_t skip_string(const char* s, size_t pos, size_t len) { + if (pos >= len || s[pos] != '"') return pos; + pos++; /* Skip opening quote */ + + while (pos < len) { + if (s[pos] == '\\' && pos + 1 < len) { + pos += 2; /* Skip escape sequence */ + } else if (s[pos] == '"') { + return pos + 1; /* Skip closing quote */ + } else { + pos++; + } + } + + return pos; +} + +/** + * @brief Skip a JSON value (object, array, string, number, boolean, null) + */ +static size_t skip_value(const char* s, size_t pos, size_t len) { + pos = skip_whitespace(s, pos, len); + if (pos >= len) return pos; + + char c = s[pos]; + + if (c == '"') { + return skip_string(s, pos, len); + } + + if (c == '{') { + int depth = 1; + pos++; + while (pos < len && depth > 0) { + c = s[pos]; + if (c == '"') { + pos = skip_string(s, pos, len); + } else if (c == '{') { + depth++; + pos++; + } else if (c == '}') { + depth--; + pos++; + } else { + pos++; + } + } + return pos; + } + + if (c == '[') { + int depth = 1; + pos++; + while (pos < len && depth > 0) { + c = s[pos]; + if (c == '"') { + pos = skip_string(s, pos, len); + } else if (c == '[') { + depth++; + pos++; + } else if (c == ']') { + depth--; + pos++; + } else { + pos++; + } + } + return pos; + } + + /* Number, boolean, or null */ + while (pos < len && !isspace((unsigned char)s[pos]) && + s[pos] != ',' && s[pos] != '}' && s[pos] != ']') { + pos++; + } + + return pos; +} + +/** + * @brief Find the offset of a JSON path component + * + * @param source The JSON source string + * @param source_len Length of source + * @param start_offset Starting offset (should point to '{' or '[') + * @param component Path component (property name or array index as string) + * @param is_array Whether we're in an array context + * @return Offset of the value, or (size_t)-1 if not found + */ +static size_t find_path_component(const char* source, size_t source_len, + size_t start_offset, const char* component, + bool is_array) { + size_t pos = skip_whitespace(source, start_offset, source_len); + if (pos >= source_len) return (size_t)-1; + + if (is_array) { + /* Array index - parse component as number */ + char* endptr; + long index = strtol(component, &endptr, 10); + if (*endptr != '\0' || index < 0) return (size_t)-1; + + if (source[pos] != '[') return (size_t)-1; + pos++; /* Skip '[' */ + + for (long i = 0; i <= index; i++) { + pos = skip_whitespace(source, pos, source_len); + if (pos >= source_len) return (size_t)-1; + + if (source[pos] == ']') return (size_t)-1; /* Array ended early */ + + if (i == index) { + return pos; /* Found the target element */ + } + + /* Skip this value and the comma */ + pos = skip_value(source, pos, source_len); + pos = skip_whitespace(source, pos, source_len); + if (pos < source_len && source[pos] == ',') { + pos++; + } + } + + return (size_t)-1; + } else { + /* Object property */ + if (source[pos] != '{') return (size_t)-1; + pos++; /* Skip '{' */ + + while (pos < source_len) { + pos = skip_whitespace(source, pos, source_len); + if (pos >= source_len || source[pos] == '}') return (size_t)-1; + + /* Read property name */ + if (source[pos] != '"') return (size_t)-1; + size_t name_start = pos + 1; + pos = skip_string(source, pos, source_len); + size_t name_end = pos - 1; + + /* Compare property name */ + size_t name_len = name_end - name_start; + bool matches = (strlen(component) == name_len && + strncmp(source + name_start, component, name_len) == 0); + + /* Skip colon */ + pos = skip_whitespace(source, pos, source_len); + if (pos >= source_len || source[pos] != ':') return (size_t)-1; + pos++; + + pos = skip_whitespace(source, pos, source_len); + + if (matches) { + return pos; /* Found the property value */ + } + + /* Skip the value */ + pos = skip_value(source, pos, source_len); + pos = skip_whitespace(source, pos, source_len); + if (pos < source_len && source[pos] == ',') { + pos++; + } + } + + return (size_t)-1; + } +} + +/** + * @brief Get the source location for a JSON path + * + * @param source The JSON source string + * @param path JSON path (e.g., "properties.name" or "[0].value") + * @return Location of the value at the path, or invalid location if not found + */ +js_location_t js_get_path_location(const char* source, const char* path) { + js_location_t invalid = {0, 0, 0}; + + if (!source || !path) return invalid; + + size_t source_len = strlen(source); + if (source_len == 0) return invalid; + + /* Initialize source locator */ + js_source_locator_t locator; + if (!js_source_locator_init(&locator, source)) return invalid; + + /* Start at the root */ + size_t pos = skip_whitespace(source, 0, source_len); + + /* If path is empty, return root location */ + if (path[0] == '\0') { + js_location_t loc = js_source_locator_get_location(&locator, pos); + js_source_locator_cleanup(&locator); + return loc; + } + + /* Parse path and navigate */ + const char* p = path; + char component[256]; + + while (*p) { + /* Skip leading dot */ + if (*p == '.') p++; + + bool is_array = (*p == '['); + if (is_array) { + p++; /* Skip '[' */ + size_t i = 0; + while (*p && *p != ']' && i < sizeof(component) - 1) { + component[i++] = *p++; + } + component[i] = '\0'; + if (*p == ']') p++; + } else { + size_t i = 0; + while (*p && *p != '.' && *p != '[' && i < sizeof(component) - 1) { + component[i++] = *p++; + } + component[i] = '\0'; + } + + if (component[0] == '\0') break; + + pos = find_path_component(source, source_len, pos, component, is_array); + if (pos == (size_t)-1) { + js_source_locator_cleanup(&locator); + return invalid; + } + } + + js_location_t loc = js_source_locator_get_location(&locator, pos); + js_source_locator_cleanup(&locator); + return loc; +} + +/* ============================================================================ + * Library Initialization + * ============================================================================ */ + +void js_init(void) { + /* Initialize allocator mutex */ + extern void js_init_allocator_mutex(void); + js_init_allocator_mutex(); +} + +void js_init_with_allocator(js_allocator_t alloc) { + /* Initialize allocator mutex first */ + extern void js_init_allocator_mutex(void); + js_init_allocator_mutex(); + + /* Then set the custom allocator */ + js_set_allocator(alloc); +} + +void js_cleanup(void) { + /* Clear regex cache to free all compiled patterns */ + extern void js_regex_cache_clear(void); + js_regex_cache_clear(); + + /* Reset to default allocator */ + js_allocator_t default_alloc = {NULL, NULL, NULL, NULL}; + js_set_allocator(default_alloc); + + /* Destroy allocator mutex */ + extern void js_destroy_allocator_mutex(void); + js_destroy_allocator_mutex(); +} + +/* ============================================================================ + * Error Message Lookup + * ============================================================================ */ + +const char* js_error_message(js_error_code_t code) { + /* Map generic error codes to messages */ + switch (code) { + case JS_ERROR_NONE: + return "No error"; + case JS_ERROR_INVALID_ARGUMENT: + return "Invalid argument"; + case JS_ERROR_OUT_OF_MEMORY: + return "Out of memory"; + case JS_ERROR_PARSE_ERROR: + return "JSON parse error"; + case JS_ERROR_INTERNAL_ERROR: + return "Internal error"; + default: + return "Unknown error"; + } +} diff --git a/r/src/json_structure/error_codes.h b/r/src/json_structure/error_codes.h new file mode 100644 index 0000000..4c34523 --- /dev/null +++ b/r/src/json_structure/error_codes.h @@ -0,0 +1,267 @@ +/** + * @file error_codes.h + * @brief Error codes for JSON Structure validation + * + * These error codes match the standardized codes in assets/error-messages.json. + * + * Copyright (c) 2024 JSON Structure Contributors + * SPDX-License-Identifier: MIT + */ + +#ifndef JSON_STRUCTURE_ERROR_CODES_H +#define JSON_STRUCTURE_ERROR_CODES_H + +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Schema validation error codes + */ +typedef enum { + /* General schema errors */ + JS_SCHEMA_OK = 0, + JS_SCHEMA_NULL, + JS_SCHEMA_INVALID_TYPE, + JS_SCHEMA_MAX_DEPTH_EXCEEDED, + JS_SCHEMA_ROOT_MISSING_ID, + JS_SCHEMA_ROOT_MISSING_NAME, + JS_SCHEMA_ROOT_MISSING_SCHEMA, + JS_SCHEMA_ROOT_MISSING_TYPE, + JS_SCHEMA_KEYWORD_EMPTY, + JS_SCHEMA_NAME_INVALID, + + /* Type errors */ + JS_SCHEMA_TYPE_INVALID, + JS_SCHEMA_TYPE_NOT_STRING, + JS_SCHEMA_TYPE_ARRAY_EMPTY, + JS_SCHEMA_TYPE_OBJECT_MISSING_REF, + + /* Reference errors */ + JS_SCHEMA_REF_NOT_FOUND, + JS_SCHEMA_REF_NOT_STRING, + JS_SCHEMA_REF_CIRCULAR, + JS_SCHEMA_REF_INVALID, + + /* Definition errors */ + JS_SCHEMA_DEFINITIONS_MUST_BE_OBJECT, + JS_SCHEMA_DEFINITION_MISSING_TYPE, + JS_SCHEMA_DEFINITION_INVALID, + + /* Object property errors */ + JS_SCHEMA_PROPERTIES_MUST_BE_OBJECT, + JS_SCHEMA_PROPERTY_INVALID, + JS_SCHEMA_REQUIRED_MUST_BE_ARRAY, + JS_SCHEMA_REQUIRED_ITEM_MUST_BE_STRING, + JS_SCHEMA_REQUIRED_PROPERTY_NOT_DEFINED, + JS_SCHEMA_ADDITIONAL_PROPERTIES_INVALID, + + /* Array/Set errors */ + JS_SCHEMA_ARRAY_MISSING_ITEMS, + JS_SCHEMA_ITEMS_INVALID, + + /* Map errors */ + JS_SCHEMA_MAP_MISSING_VALUES, + JS_SCHEMA_VALUES_INVALID, + + /* Tuple errors */ + JS_SCHEMA_TUPLE_MISSING_DEFINITION, + JS_SCHEMA_TUPLE_MISSING_PROPERTIES, + JS_SCHEMA_TUPLE_INVALID_FORMAT, + JS_SCHEMA_TUPLE_PROPERTY_NOT_DEFINED, + + /* Choice errors */ + JS_SCHEMA_CHOICE_MISSING_CHOICES, + JS_SCHEMA_CHOICES_NOT_OBJECT, + JS_SCHEMA_CHOICE_INVALID, + JS_SCHEMA_SELECTOR_NOT_STRING, + + /* Enum/Const errors */ + JS_SCHEMA_ENUM_NOT_ARRAY, + JS_SCHEMA_ENUM_EMPTY, + JS_SCHEMA_ENUM_DUPLICATES, + JS_SCHEMA_CONST_INVALID, + + /* Extension errors */ + JS_SCHEMA_USES_NOT_ARRAY, + JS_SCHEMA_USES_INVALID_EXTENSION, + JS_SCHEMA_OFFERS_NOT_ARRAY, + JS_SCHEMA_OFFERS_INVALID_EXTENSION, + JS_SCHEMA_EXTENSION_KEYWORD_WITHOUT_USES, + + /* Constraint errors */ + JS_SCHEMA_MIN_MAX_INVALID, + JS_SCHEMA_MINLENGTH_INVALID, + JS_SCHEMA_MAXLENGTH_INVALID, + JS_SCHEMA_PATTERN_INVALID, + JS_SCHEMA_FORMAT_INVALID, + JS_SCHEMA_CONSTRAINT_TYPE_MISMATCH, + JS_SCHEMA_MINIMUM_EXCEEDS_MAXIMUM, + JS_SCHEMA_MINLENGTH_EXCEEDS_MAXLENGTH, + JS_SCHEMA_MINLENGTH_NEGATIVE, + JS_SCHEMA_MAXLENGTH_NEGATIVE, + JS_SCHEMA_MINITEMS_EXCEEDS_MAXITEMS, + JS_SCHEMA_MINITEMS_NEGATIVE, + JS_SCHEMA_MULTIPLEOF_INVALID, + JS_SCHEMA_KEYWORD_INVALID_TYPE, + JS_SCHEMA_CONSTRAINT_VALUE_INVALID, + + /* Import errors */ + JS_SCHEMA_IMPORT_NOT_ALLOWED, + JS_SCHEMA_IMPORT_FAILED, + JS_SCHEMA_IMPORT_CIRCULAR, + + /* Composition errors */ + JS_SCHEMA_ALLOF_NOT_ARRAY, + JS_SCHEMA_ANYOF_NOT_ARRAY, + JS_SCHEMA_ONEOF_NOT_ARRAY, + JS_SCHEMA_NOT_INVALID, + JS_SCHEMA_IF_INVALID, + JS_SCHEMA_THEN_WITHOUT_IF, + JS_SCHEMA_ELSE_WITHOUT_IF, + + JS_SCHEMA_ERROR_COUNT +} js_schema_error_t; + +/** + * @brief Instance validation error codes + */ +typedef enum { + /* Success */ + JS_INSTANCE_OK = 0, + + /* Type mismatch errors */ + JS_INSTANCE_TYPE_MISMATCH, + JS_INSTANCE_TYPE_UNKNOWN, + + /* String errors */ + JS_INSTANCE_STRING_EXPECTED, + JS_INSTANCE_STRING_TOO_SHORT, + JS_INSTANCE_STRING_TOO_LONG, + JS_INSTANCE_STRING_PATTERN_MISMATCH, + JS_INSTANCE_STRING_FORMAT_INVALID, + + /* Number errors */ + JS_INSTANCE_NUMBER_EXPECTED, + JS_INSTANCE_NUMBER_TOO_SMALL, + JS_INSTANCE_NUMBER_TOO_LARGE, + JS_INSTANCE_NUMBER_NOT_MULTIPLE, + JS_INSTANCE_INTEGER_EXPECTED, + JS_INSTANCE_INTEGER_OUT_OF_RANGE, + + /* Boolean/Null errors */ + JS_INSTANCE_BOOLEAN_EXPECTED, + JS_INSTANCE_NULL_EXPECTED, + + /* Object errors */ + JS_INSTANCE_OBJECT_EXPECTED, + JS_INSTANCE_REQUIRED_MISSING, + JS_INSTANCE_ADDITIONAL_PROPERTY, + JS_INSTANCE_PROPERTY_INVALID, + JS_INSTANCE_TOO_FEW_PROPERTIES, + JS_INSTANCE_TOO_MANY_PROPERTIES, + JS_INSTANCE_DEPENDENT_REQUIRED_MISSING, + + /* Array errors */ + JS_INSTANCE_ARRAY_EXPECTED, + JS_INSTANCE_ARRAY_TOO_SHORT, + JS_INSTANCE_ARRAY_TOO_LONG, + JS_INSTANCE_ARRAY_NOT_UNIQUE, + JS_INSTANCE_ARRAY_CONTAINS_MISSING, + JS_INSTANCE_ARRAY_CONTAINS_TOO_FEW, + JS_INSTANCE_ARRAY_CONTAINS_TOO_MANY, + JS_INSTANCE_ARRAY_ITEM_INVALID, + + /* Tuple errors */ + JS_INSTANCE_TUPLE_EXPECTED, + JS_INSTANCE_TUPLE_LENGTH_MISMATCH, + JS_INSTANCE_TUPLE_ELEMENT_INVALID, + + /* Map errors */ + JS_INSTANCE_MAP_EXPECTED, + JS_INSTANCE_MAP_VALUE_INVALID, + JS_INSTANCE_MAP_TOO_FEW_ENTRIES, + JS_INSTANCE_MAP_TOO_MANY_ENTRIES, + JS_INSTANCE_MAP_KEY_PATTERN_MISMATCH, + + /* Set errors */ + JS_INSTANCE_SET_EXPECTED, + JS_INSTANCE_SET_NOT_UNIQUE, + JS_INSTANCE_SET_ITEM_INVALID, + + /* Choice errors */ + JS_INSTANCE_CHOICE_NO_MATCH, + JS_INSTANCE_CHOICE_MULTIPLE_MATCHES, + JS_INSTANCE_CHOICE_UNKNOWN, + JS_INSTANCE_CHOICE_SELECTOR_MISSING, + JS_INSTANCE_CHOICE_SELECTOR_INVALID, + + /* Enum/Const errors */ + JS_INSTANCE_ENUM_MISMATCH, + JS_INSTANCE_CONST_MISMATCH, + + /* Date/Time errors */ + JS_INSTANCE_DATE_EXPECTED, + JS_INSTANCE_DATE_INVALID, + JS_INSTANCE_TIME_EXPECTED, + JS_INSTANCE_TIME_INVALID, + JS_INSTANCE_DATETIME_EXPECTED, + JS_INSTANCE_DATETIME_INVALID, + JS_INSTANCE_DURATION_EXPECTED, + JS_INSTANCE_DURATION_INVALID, + + /* UUID errors */ + JS_INSTANCE_UUID_EXPECTED, + JS_INSTANCE_UUID_INVALID, + + /* URI errors */ + JS_INSTANCE_URI_EXPECTED, + JS_INSTANCE_URI_INVALID, + + /* Binary errors */ + JS_INSTANCE_BINARY_EXPECTED, + JS_INSTANCE_BINARY_INVALID, + + /* JSON Pointer errors */ + JS_INSTANCE_JSONPOINTER_EXPECTED, + JS_INSTANCE_JSONPOINTER_INVALID, + + /* Composition errors */ + JS_INSTANCE_ALLOF_FAILED, + JS_INSTANCE_ANYOF_FAILED, + JS_INSTANCE_ONEOF_FAILED, + JS_INSTANCE_ONEOF_MULTIPLE, + JS_INSTANCE_NOT_FAILED, + JS_INSTANCE_IF_THEN_FAILED, + JS_INSTANCE_IF_ELSE_FAILED, + + /* Reference errors */ + JS_INSTANCE_REF_NOT_FOUND, + + /* Union errors */ + JS_INSTANCE_UNION_NO_MATCH, + + JS_INSTANCE_ERROR_COUNT +} js_instance_error_t; + +/** + * @brief Get the string representation of a schema error code + * @param code The error code + * @return Constant string (do not free) + */ +JS_API const char* js_schema_error_str(js_schema_error_t code); + +/** + * @brief Get the string representation of an instance error code + * @param code The error code + * @return Constant string (do not free) + */ +JS_API const char* js_instance_error_str(js_instance_error_t code); + +#ifdef __cplusplus +} +#endif + +#endif /* JSON_STRUCTURE_ERROR_CODES_H */ diff --git a/r/src/json_structure/export.h b/r/src/json_structure/export.h new file mode 100644 index 0000000..9fc5c28 --- /dev/null +++ b/r/src/json_structure/export.h @@ -0,0 +1,51 @@ +/** + * @file export.h + * @brief DLL export/import macros for JSON Structure SDK + * + * This header provides the JS_API macro for proper symbol visibility + * when building or using the library as a shared library (DLL). + * + * When building the library as a shared library: + * - Define JS_BUILDING_SHARED when compiling the library sources + * - JS_API will expand to __declspec(dllexport) on Windows + * + * When using the library as a shared library: + * - Define JS_USING_SHARED when compiling your application + * - JS_API will expand to __declspec(dllimport) on Windows + * + * For static library builds (default): + * - Define neither macro + * - JS_API will be empty + * + * On non-Windows platforms, JS_API uses GCC visibility attributes when available. + * + * Copyright (c) 2024 JSON Structure Contributors + * SPDX-License-Identifier: MIT + */ + +#ifndef JSON_STRUCTURE_EXPORT_H +#define JSON_STRUCTURE_EXPORT_H + +/* Determine symbol visibility/export settings */ +#if defined(_WIN32) || defined(__CYGWIN__) + /* Windows platform */ + #ifdef JS_BUILDING_SHARED + /* Building the DLL */ + #define JS_API __declspec(dllexport) + #elif defined(JS_USING_SHARED) + /* Using the DLL */ + #define JS_API __declspec(dllimport) + #else + /* Static library */ + #define JS_API + #endif +#else + /* Unix-like platforms */ + #if defined(__GNUC__) && __GNUC__ >= 4 + #define JS_API __attribute__((visibility("default"))) + #else + #define JS_API + #endif +#endif + +#endif /* JSON_STRUCTURE_EXPORT_H */ diff --git a/r/src/json_structure/instance_validator.h b/r/src/json_structure/instance_validator.h new file mode 100644 index 0000000..79b2ad5 --- /dev/null +++ b/r/src/json_structure/instance_validator.h @@ -0,0 +1,117 @@ +/** + * @file instance_validator.h + * @brief JSON Structure instance validator + * + * Validates JSON instances against JSON Structure schemas. + * + * Copyright (c) 2024 JSON Structure Contributors + * SPDX-License-Identifier: MIT + */ + +#ifndef JSON_STRUCTURE_INSTANCE_VALIDATOR_H +#define JSON_STRUCTURE_INSTANCE_VALIDATOR_H + +#include "types.h" +#include "error_codes.h" +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ============================================================================ + * Import Support Types + * ============================================================================ */ + +/** + * @brief Entry for mapping URIs to file paths or schemas + * + * Used for resolving $import and $importdefs keywords. + */ +typedef struct js_import_entry { + const char* uri; /**< The URI to match (e.g., "http://example.com/schema.json") */ + const char* file_path; /**< File path to load schema from (NULL if schema provided) */ + const cJSON* schema; /**< Pre-parsed schema (NULL if file_path provided) */ +} js_import_entry_t; + +/** + * @brief Registry for external schemas used during import resolution + */ +typedef struct js_import_registry { + const js_import_entry_t* entries; /**< Array of import entries */ + size_t count; /**< Number of entries */ +} js_import_registry_t; + +/* ============================================================================ + * Instance Validator Options + * ============================================================================ */ + +/** + * @brief Options for instance validation + */ +typedef struct js_instance_options { + bool allow_additional_properties; /**< Allow properties not in schema (default true) */ + bool validate_formats; /**< Validate format constraints (default true) */ + bool allow_import; /**< Allow $import and $importdefs keywords (default false) */ + const js_import_registry_t* import_registry; /**< External schemas for import resolution (optional) */ +} js_instance_options_t; + +/** Default options */ +#define JS_INSTANCE_OPTIONS_DEFAULT ((js_instance_options_t){true, true, false, NULL}) + +/* ============================================================================ + * Instance Validator + * ============================================================================ */ + +/** + * @brief Instance validator + */ +typedef struct js_instance_validator { + js_instance_options_t options; +} js_instance_validator_t; + +/** + * @brief Initialize an instance validator with default options + * @param validator Validator to initialize + */ +JS_API void js_instance_validator_init(js_instance_validator_t* validator); + +/** + * @brief Initialize an instance validator with custom options + * @param validator Validator to initialize + * @param options Validation options + */ +JS_API void js_instance_validator_init_with_options(js_instance_validator_t* validator, + js_instance_options_t options); + +/** + * @brief Validate an instance against a schema (both as strings) + * @param validator Validator instance + * @param instance_json JSON string containing the instance + * @param schema_json JSON string containing the schema + * @param result Output validation result + * @return true if instance is valid, false otherwise + */ +JS_API bool js_instance_validate_strings(const js_instance_validator_t* validator, + const char* instance_json, + const char* schema_json, + js_result_t* result); + +/** + * @brief Validate a pre-parsed instance against a pre-parsed schema + * @param validator Validator instance + * @param instance Parsed cJSON object representing the instance + * @param schema Parsed cJSON object representing the schema + * @param result Output validation result + * @return true if instance is valid, false otherwise + */ +JS_API bool js_instance_validate(const js_instance_validator_t* validator, + const cJSON* instance, + const cJSON* schema, + js_result_t* result); + +#ifdef __cplusplus +} +#endif + +#endif /* JSON_STRUCTURE_INSTANCE_VALIDATOR_H */ diff --git a/r/src/json_structure/json.h b/r/src/json_structure/json.h new file mode 100644 index 0000000..5c2eb9c --- /dev/null +++ b/r/src/json_structure/json.h @@ -0,0 +1,334 @@ +/** + * @file json.h + * @brief Compact JSON parser for JSON Structure validation + * + * This is a minimal, zero-copy JSON parser optimized for validation. + * It uses string views and does not allocate memory for parsed values. + * + * Copyright (c) 2024 JSON Structure Contributors + * SPDX-License-Identifier: MIT + */ + +#ifndef JSON_STRUCTURE_JSON_H +#define JSON_STRUCTURE_JSON_H + +#include "types.h" +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ============================================================================ + * JSON Value Types + * ============================================================================ */ + +/** + * @brief JSON value type + */ +typedef enum js_json_type { + JS_JSON_NULL = 0, + JS_JSON_BOOL, + JS_JSON_NUMBER, + JS_JSON_STRING, + JS_JSON_ARRAY, + JS_JSON_OBJECT +} js_json_type_t; + +/* ============================================================================ + * JSON Value (non-owning view into parsed JSON) + * ============================================================================ */ + +/** + * @brief JSON value - a view into parsed JSON data + * + * This is a 32-byte structure that provides a view into the original + * JSON string. No memory is allocated for values. + */ +typedef struct js_json { + js_json_type_t type; /**< Value type */ + js_str_t raw; /**< Raw JSON text for this value */ + union { + bool boolean; /**< Boolean value */ + double number; /**< Numeric value (also stores integers) */ + js_str_t string; /**< String value (unescaped view) */ + struct { + uint32_t count; /**< Number of elements/properties */ + uint32_t _reserved; + } container; + } data; +} js_json_t; + +/* ============================================================================ + * JSON Parser + * ============================================================================ */ + +/** + * @brief JSON parser state + */ +typedef struct js_parser { + const char* json; /**< Original JSON string */ + size_t len; /**< Length of JSON string */ + size_t pos; /**< Current position */ + uint32_t line; /**< Current line (1-indexed) */ + uint32_t column; /**< Current column (1-indexed) */ + const js_allocator_t* alloc;/**< Allocator for internal structures */ + char error[128]; /**< Error message if parsing fails */ +} js_parser_t; + +/** + * @brief Initialize parser with JSON string + * @param parser Parser to initialize + * @param json JSON string (must remain valid during parsing) + * @param len Length of JSON string + * @param alloc Allocator (NULL for default) + */ +JS_API void js_parser_init(js_parser_t* parser, const char* json, size_t len, + const js_allocator_t* alloc); + +/** + * @brief Parse a JSON value + * @param parser Parser state + * @param out Output value + * @return true on success, false on parse error + */ +JS_API bool js_parser_parse(js_parser_t* parser, js_json_t* out); + +/** + * @brief Get current source location + * @param parser Parser state + * @return Current location + */ +JS_API js_location_t js_parser_location(const js_parser_t* parser); + +/* ============================================================================ + * JSON Value Access + * ============================================================================ */ + +/** + * @brief Check if value is null + */ +static inline bool js_json_is_null(const js_json_t* v) { + return v && v->type == JS_JSON_NULL; +} + +/** + * @brief Check if value is boolean + */ +static inline bool js_json_is_bool(const js_json_t* v) { + return v && v->type == JS_JSON_BOOL; +} + +/** + * @brief Check if value is number + */ +static inline bool js_json_is_number(const js_json_t* v) { + return v && v->type == JS_JSON_NUMBER; +} + +/** + * @brief Check if value is string + */ +static inline bool js_json_is_string(const js_json_t* v) { + return v && v->type == JS_JSON_STRING; +} + +/** + * @brief Check if value is array + */ +static inline bool js_json_is_array(const js_json_t* v) { + return v && v->type == JS_JSON_ARRAY; +} + +/** + * @brief Check if value is object + */ +static inline bool js_json_is_object(const js_json_t* v) { + return v && v->type == JS_JSON_OBJECT; +} + +/** + * @brief Get boolean value + * @param v JSON value + * @param def Default if not boolean + * @return Boolean value or default + */ +static inline bool js_json_get_bool(const js_json_t* v, bool def) { + return (v && v->type == JS_JSON_BOOL) ? v->data.boolean : def; +} + +/** + * @brief Get number value + * @param v JSON value + * @param def Default if not number + * @return Number value or default + */ +static inline double js_json_get_number(const js_json_t* v, double def) { + return (v && v->type == JS_JSON_NUMBER) ? v->data.number : def; +} + +/** + * @brief Get string value + * @param v JSON value + * @return String view or empty + */ +static inline js_str_t js_json_get_string(const js_json_t* v) { + return (v && v->type == JS_JSON_STRING) ? v->data.string : JS_STR_EMPTY; +} + +/** + * @brief Get array/object element count + * @param v JSON value + * @return Element count or 0 + */ +static inline uint32_t js_json_get_count(const js_json_t* v) { + return (v && (v->type == JS_JSON_ARRAY || v->type == JS_JSON_OBJECT)) + ? v->data.container.count : 0; +} + +/* ============================================================================ + * JSON Object/Array Iteration + * ============================================================================ */ + +/** + * @brief Iterator for JSON objects and arrays + */ +typedef struct js_json_iter { + js_parser_t parser; /**< Internal parser state */ + uint32_t index; /**< Current index */ + uint32_t count; /**< Total count */ + bool is_object; /**< True if iterating object */ +} js_json_iter_t; + +/** + * @brief Initialize iterator for array or object + * @param iter Iterator to initialize + * @param value JSON array or object value + * @return true if iteration can begin + */ +JS_API bool js_json_iter_init(js_json_iter_t* iter, const js_json_t* value); + +/** + * @brief Get next value from iterator + * @param iter Iterator + * @param key Output key (only for objects, NULL for arrays) + * @param value Output value + * @return true if value available, false if end + */ +JS_API bool js_json_iter_next(js_json_iter_t* iter, js_str_t* key, js_json_t* value); + +/* ============================================================================ + * JSON Object Property Access + * ============================================================================ */ + +/** + * @brief Get property from JSON object by name + * @param obj JSON object value + * @param key Property name + * @param out Output value + * @return true if property found + */ +JS_API bool js_json_get(const js_json_t* obj, js_str_t key, js_json_t* out); + +/** + * @brief Get property from JSON object by C string + * @param obj JSON object value + * @param key Property name (null-terminated) + * @param out Output value + * @return true if property found + */ +JS_API bool js_json_get_cstr(const js_json_t* obj, const char* key, js_json_t* out); + +/** + * @brief Check if object has property + * @param obj JSON object value + * @param key Property name + * @return true if property exists + */ +JS_API bool js_json_has(const js_json_t* obj, js_str_t key); + +/** + * @brief Check if object has property (C string) + * @param obj JSON object value + * @param key Property name (null-terminated) + * @return true if property exists + */ +JS_API bool js_json_has_cstr(const js_json_t* obj, const char* key); + +/* ============================================================================ + * JSON Array Element Access + * ============================================================================ */ + +/** + * @brief Get array element by index + * @param arr JSON array value + * @param index Element index + * @param out Output value + * @return true if element found + */ +JS_API bool js_json_get_index(const js_json_t* arr, uint32_t index, js_json_t* out); + +/* ============================================================================ + * JSON String Utilities + * ============================================================================ */ + +/** + * @brief Compare JSON string to C string + * @param v JSON string value + * @param cstr C string to compare + * @return true if equal + */ +JS_API bool js_json_str_eq(const js_json_t* v, const char* cstr); + +/** + * @brief Unescape JSON string into buffer + * @param str JSON string view (with escapes) + * @param buf Output buffer + * @param buf_size Buffer size + * @return Length written, or required size if buf_size is 0 + */ +JS_API size_t js_json_unescape(js_str_t str, char* buf, size_t buf_size); + +/* ============================================================================ + * JSON Source Location + * ============================================================================ */ + +/** + * @brief Location map for tracking JSON paths to source locations + */ +typedef struct js_locator { + const char* json; + size_t len; + /* Internal location cache - lazily built */ + void* _cache; + const js_allocator_t* alloc; +} js_locator_t; + +/** + * @brief Initialize source locator + * @param loc Locator to initialize + * @param json JSON string + * @param len JSON string length + * @param alloc Allocator (NULL for default) + */ +JS_API void js_locator_init(js_locator_t* loc, const char* json, size_t len, + const js_allocator_t* alloc); + +/** + * @brief Free locator resources + */ +JS_API void js_locator_cleanup(js_locator_t* loc); + +/** + * @brief Get source location for a JSON path + * @param loc Locator + * @param path JSON Pointer path + * @return Source location + */ +JS_API js_location_t js_locator_find(js_locator_t* loc, const char* path); + +#ifdef __cplusplus +} +#endif + +#endif /* JSON_STRUCTURE_JSON_H */ diff --git a/r/src/json_structure/json_structure.h b/r/src/json_structure/json_structure.h new file mode 100644 index 0000000..f4dd9a3 --- /dev/null +++ b/r/src/json_structure/json_structure.h @@ -0,0 +1,124 @@ +/** + * @file json_structure.h + * @brief JSON Structure SDK main header + * + * This is the main umbrella header for the JSON Structure SDK. + * Include this header to get access to all SDK functionality. + * + * Copyright (c) 2024 JSON Structure Contributors + * SPDX-License-Identifier: MIT + */ + +#ifndef JSON_STRUCTURE_H +#define JSON_STRUCTURE_H + +/* Version information */ +#define JSON_STRUCTURE_VERSION_MAJOR 0 +#define JSON_STRUCTURE_VERSION_MINOR 1 +#define JSON_STRUCTURE_VERSION_PATCH 0 +#define JSON_STRUCTURE_VERSION_STRING "0.1.0" + +/* Core headers */ +#include "export.h" +#include "types.h" +#include "error_codes.h" +#include "schema_validator.h" +#include "instance_validator.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ============================================================================ + * Library Initialization + * ============================================================================ */ + +/** + * @brief Initialize the JSON Structure library + * + * Call this once at program startup. This function is optional if you + * don't need custom memory allocation, but it's recommended for explicit + * initialization of internal resources. + * + * @note Thread-safety: This function should be called once before any + * validation operations. It initializes internal synchronization + * primitives used for thread-safe operation. + */ +JS_API void js_init(void); + +/** + * @brief Initialize the JSON Structure library with custom allocator + * @param alloc Custom allocator functions + * + * Call this once at program startup if you need custom memory allocation. + * This function initializes the library and sets the custom allocator. + * + * @note Thread-safety: This function should be called once before any + * validation operations. Do not call this concurrently from multiple + * threads or while validation is in progress. + */ +JS_API void js_init_with_allocator(js_allocator_t alloc); + +/** + * @brief Clean up the JSON Structure library + * + * Call this at program shutdown to release any internal resources, + * including the regex compilation cache and synchronization primitives. + * + * After calling js_cleanup(), you can call js_init() or + * js_init_with_allocator() again to reinitialize the library if needed. + * + * @note Thread-safety: This function must only be called when no + * validation operations are in progress. Calling this while + * validations are running leads to undefined behavior. + * + * @note The internal mutex is initialized once using pthread_once (Unix) + * or InitOnceExecuteOnce (Windows) for thread safety. While the + * library can be reinitialized after cleanup, the one-time + * initialization mechanism persists for the process lifetime. + */ +JS_API void js_cleanup(void); + +/* ============================================================================ + * Convenience Functions + * ============================================================================ */ + +/** + * @brief Validate a schema string (convenience function) + * @param schema_json JSON string containing the schema + * @param result Output validation result + * @return true if schema is valid, false otherwise + */ +static inline bool js_validate_schema(const char* schema_json, js_result_t* result) { + js_schema_validator_t validator; + js_schema_validator_init(&validator); + return js_schema_validate_string(&validator, schema_json, result); +} + +/** + * @brief Validate an instance against a schema (convenience function) + * @param instance_json JSON string containing the instance + * @param schema_json JSON string containing the schema + * @param result Output validation result + * @return true if instance is valid, false otherwise + */ +static inline bool js_validate_instance(const char* instance_json, + const char* schema_json, + js_result_t* result) { + js_instance_validator_t validator; + js_instance_validator_init(&validator); + return js_instance_validate_strings(&validator, instance_json, schema_json, result); +} + +/** + * @brief Get the JSON Structure error message for an error code + * @param code Error code + * @return Human-readable error message + */ +JS_API const char* js_error_message(js_error_code_t code); + +#ifdef __cplusplus +} +#endif + +#endif /* JSON_STRUCTURE_H */ diff --git a/r/src/json_structure/schema_validator.h b/r/src/json_structure/schema_validator.h new file mode 100644 index 0000000..3393898 --- /dev/null +++ b/r/src/json_structure/schema_validator.h @@ -0,0 +1,115 @@ +/** + * @file schema_validator.h + * @brief JSON Structure schema validator + * + * Validates JSON Structure schema documents for correctness. + * + * Copyright (c) 2024 JSON Structure Contributors + * SPDX-License-Identifier: MIT + */ + +#ifndef JSON_STRUCTURE_SCHEMA_VALIDATOR_H +#define JSON_STRUCTURE_SCHEMA_VALIDATOR_H + +#include "types.h" +#include "error_codes.h" +#include "export.h" +#include "instance_validator.h" /* For js_import_registry_t */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* ============================================================================ + * Schema Validator Options + * ============================================================================ */ + +/** + * @brief Options for schema validation + */ +typedef struct js_schema_options { + bool allow_import; /**< Allow $import and $importdefs keywords */ + bool warnings_enabled; /**< Report warnings (e.g., unused validation keywords) */ + const js_import_registry_t* import_registry; /**< External schemas for import resolution (optional) */ +} js_schema_options_t; + +/** Default options */ +#define JS_SCHEMA_OPTIONS_DEFAULT ((js_schema_options_t){false, true, NULL}) + +/* ============================================================================ + * Schema Validator + * ============================================================================ */ + +/** + * @brief Schema validator instance + */ +typedef struct js_schema_validator { + js_schema_options_t options; +} js_schema_validator_t; + +/** + * @brief Initialize a schema validator with default options + * @param validator Validator to initialize + */ +JS_API void js_schema_validator_init(js_schema_validator_t* validator); + +/** + * @brief Initialize a schema validator with custom options + * @param validator Validator to initialize + * @param options Validation options + */ +JS_API void js_schema_validator_init_with_options(js_schema_validator_t* validator, + js_schema_options_t options); + +/** + * @brief Validate a schema from a JSON string + * @param validator Validator instance + * @param json_str JSON string containing the schema + * @param result Output validation result + * @return true if schema is valid (no errors), false otherwise + */ +JS_API bool js_schema_validate_string(const js_schema_validator_t* validator, + const char* json_str, + js_result_t* result); + +/** + * @brief Validate a pre-parsed schema + * @param validator Validator instance + * @param schema Parsed cJSON object representing the schema + * @param result Output validation result + * @return true if schema is valid (no errors), false otherwise + */ +JS_API bool js_schema_validate(const js_schema_validator_t* validator, + const cJSON* schema, + js_result_t* result); + +/** + * @brief Check if a type name is a valid primitive type + * @param type_name Type name string + * @return true if valid primitive type + */ +JS_API bool js_schema_is_valid_primitive_type(const char* type_name); + +/** + * @brief Check if a type name is a valid compound type + * @param type_name Type name string + * @return true if valid compound type + */ +JS_API bool js_schema_is_valid_compound_type(const char* type_name); + +/** + * @brief Alias for js_schema_validate_string (for C++ bindings compatibility) + * @param validator Validator instance + * @param json_str JSON string containing the schema + * @param result Output validation result + * @return true if schema is valid (no errors), false otherwise + */ +JS_API bool js_schema_validator_validate_string(const js_schema_validator_t* validator, + const char* json_str, + js_result_t* result); + +#ifdef __cplusplus +} +#endif + +#endif /* JSON_STRUCTURE_SCHEMA_VALIDATOR_H */ diff --git a/r/src/json_structure/types.h b/r/src/json_structure/types.h new file mode 100644 index 0000000..4bfc200 --- /dev/null +++ b/r/src/json_structure/types.h @@ -0,0 +1,513 @@ +/** + * @file types.h + * @brief Core type definitions for JSON Structure validation library + * + * This header provides the fundamental types used throughout the library, + * including JSON type enumerations, error handling structures, result types, + * and custom allocator support for embedded systems. + * + * @note This library requires cJSON as an external dependency for JSON parsing. + * + * SPDX-License-Identifier: MIT + */ + +#ifndef JS_TYPES_H +#define JS_TYPES_H + +#include +#include +#include + +/* cJSON is used as the external JSON parsing library */ +#include + +/* Export macros for shared library support */ +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ============================================================================ + * Version Information + * ========================================================================== */ + +#define JS_VERSION_MAJOR 0 +#define JS_VERSION_MINOR 1 +#define JS_VERSION_PATCH 0 +#define JS_VERSION_STRING "0.1.0" + +/* ============================================================================ + * Configuration Macros + * ========================================================================== */ + +/** + * @brief Maximum number of errors that can be stored in a result + * + * This can be overridden at compile time with -DJS_MAX_ERRORS=N + */ +#ifndef JS_MAX_ERRORS +#define JS_MAX_ERRORS 100 +#endif + +/** + * @brief Initial capacity for error array + */ +#ifndef JS_INITIAL_ERROR_CAPACITY +#define JS_INITIAL_ERROR_CAPACITY 16 +#endif + +/* ============================================================================ + * JSON Structure Type Enumeration + * ========================================================================== */ + +/** + * @brief JSON Structure type identifiers + * + * These types correspond to the JSON Structure specification type names. + * Use js_type_name() to get the string representation. + */ +typedef enum js_type { + /* Invalid/unknown type */ + JS_TYPE_UNKNOWN = 0, + + /* Primitive types */ + JS_TYPE_ANY, + JS_TYPE_NULL, + JS_TYPE_BOOLEAN, + JS_TYPE_INTEGER, + JS_TYPE_NUMBER, + JS_TYPE_STRING, + JS_TYPE_BINARY, + JS_TYPE_OBJECT, + JS_TYPE_ARRAY, + JS_TYPE_MAP, + JS_TYPE_SET, + JS_TYPE_ABSTRACT, + JS_TYPE_CHOICE, + JS_TYPE_TUPLE, + + /* Extended numeric types */ + JS_TYPE_INT8, + JS_TYPE_INT16, + JS_TYPE_INT32, + JS_TYPE_INT64, + JS_TYPE_UINT8, + JS_TYPE_UINT16, + JS_TYPE_UINT32, + JS_TYPE_UINT64, + JS_TYPE_FLOAT16, + JS_TYPE_FLOAT32, + JS_TYPE_FLOAT64, + JS_TYPE_FLOAT128, + JS_TYPE_DECIMAL, + JS_TYPE_DECIMAL64, + JS_TYPE_DECIMAL128, + + /* Extended string types */ + JS_TYPE_DATETIME, + JS_TYPE_DATE, + JS_TYPE_TIME, + JS_TYPE_DURATION, + JS_TYPE_UUID, + JS_TYPE_URI, + JS_TYPE_URI_REFERENCE, + JS_TYPE_URI_TEMPLATE, + JS_TYPE_REGEX, + JS_TYPE_CHAR, + JS_TYPE_IPV4, + JS_TYPE_IPV6, + JS_TYPE_EMAIL, + JS_TYPE_IDN_EMAIL, + JS_TYPE_HOSTNAME, + JS_TYPE_IDN_HOSTNAME, + JS_TYPE_IRI, + JS_TYPE_IRI_REFERENCE, + JS_TYPE_JSON_POINTER, + JS_TYPE_RELATIVE_JSON_POINTER, + + /* Internal use */ + JS_TYPE_COUNT +} js_type_t; + +/* ============================================================================ + * Severity and Location Types + * ========================================================================== */ + +/** + * @brief Severity level for validation errors/warnings + */ +typedef enum js_severity { + JS_SEVERITY_ERROR = 0, /**< Validation error - schema/instance is invalid */ + JS_SEVERITY_WARNING, /**< Warning - potential issue but not invalid */ + JS_SEVERITY_INFO /**< Informational message */ +} js_severity_t; + +/** + * @brief Source location information for error reporting + * + * Provides line, column, and byte offset for locating errors in source JSON. + */ +typedef struct js_location { + int line; /**< 1-based line number, 0 if unknown */ + int column; /**< 1-based column number, 0 if unknown */ + size_t offset; /**< 0-based byte offset from start of document */ +} js_location_t; + +/* ============================================================================ + * Generic Error Codes + * ========================================================================== */ + +/** + * @brief Generic error codes used by the library + * + * More specific error codes are defined in error_codes.h. + */ +typedef enum js_error_code { + JS_ERROR_NONE = 0, /**< No error */ + JS_ERROR_INVALID_ARGUMENT, /**< Invalid argument passed */ + JS_ERROR_OUT_OF_MEMORY, /**< Memory allocation failed */ + JS_ERROR_PARSE_ERROR, /**< JSON parsing error */ + JS_ERROR_INTERNAL_ERROR, /**< Internal library error */ + JS_ERROR_MAX_GENERIC = 100 /**< Marker: specific codes start after this */ +} js_error_code_enum_t; + +/** + * @brief Combined error code type + * + * This is a union of generic, schema, and instance error codes. + * See error_codes.h for the specific error code enumerations. + */ +typedef int js_error_code_t; + +/* ============================================================================ + * Error Structure + * ========================================================================== */ + +/** + * @brief Validation error information + * + * Contains all information about a single validation error including + * the error code, severity, location, JSON path, and human-readable message. + * + * @note The path and message fields are dynamically allocated and must be + * freed using js_error_cleanup() or managed by js_result_cleanup(). + */ +typedef struct js_error { + js_error_code_t code; /**< Error code (see error_codes.h) */ + js_severity_t severity; /**< Severity level */ + js_location_t location; /**< Source location if available */ + char* path; /**< JSON Pointer path to error location (allocated) */ + char* message; /**< Human-readable error message (allocated) */ +} js_error_t; + +/* ============================================================================ + * Result Structure + * ========================================================================== */ + +/** + * @brief Validation result container + * + * Holds the overall validation result and any accumulated errors. + * Use js_result_init() before use and js_result_cleanup() when done. + */ +typedef struct js_result { + bool valid; /**< True if validation passed */ + js_error_t* errors; /**< Array of errors (dynamically allocated) */ + size_t error_count; /**< Number of errors in the array */ + size_t error_capacity; /**< Allocated capacity for errors array */ +} js_result_t; + +/* ============================================================================ + * Custom Allocator Support + * ========================================================================== */ + +/** + * @brief Custom memory allocator interface + * + * For embedded systems or custom memory management, provide implementations + * of these function pointers. Pass to js_set_allocator() before any other + * library calls. This also configures cJSON to use the same allocator. + * + * @note If not set, standard malloc/realloc/free are used. + */ +typedef struct js_allocator { + void* (*malloc)(size_t size); /**< Allocation function */ + void* (*realloc)(void* ptr, size_t size); /**< Reallocation function */ + void (*free)(void* ptr); /**< Deallocation function */ + void* user_data; /**< Optional user context */ +} js_allocator_t; + +/* ============================================================================ + * Allocator Functions + * ========================================================================== */ + +/** + * @brief Set a custom allocator for the library + * + * This function must be called before any other library functions if you + * want to use a custom allocator. It also configures cJSON to use the + * same allocator functions. + * + * @param alloc Custom allocator with malloc, realloc, and free functions + * + * @note Pass NULL functions to reset to default allocator + * @note Thread-safety: This function is thread-safe but should only be called + * during initialization (from js_init_with_allocator() or before any + * validation calls). Changing the allocator while validation is in + * progress may lead to undefined behavior. + */ +JS_API void js_set_allocator(js_allocator_t alloc); + +/** + * @brief Get the current allocator + * + * @return Current allocator configuration + */ +JS_API js_allocator_t js_get_allocator(void); + +/** + * @brief Allocate memory using the configured allocator + * + * @param size Number of bytes to allocate + * @return Pointer to allocated memory, or NULL on failure + */ +JS_API void* js_malloc(size_t size); + +/** + * @brief Reallocate memory using the configured allocator + * + * @param ptr Pointer to existing allocation (may be NULL) + * @param size New size in bytes + * @return Pointer to reallocated memory, or NULL on failure + */ +JS_API void* js_realloc(void* ptr, size_t size); + +/** + * @brief Free memory using the configured allocator + * + * @param ptr Pointer to memory to free (may be NULL) + */ +JS_API void js_free(void* ptr); + +/** + * @brief Duplicate a string using the configured allocator + * + * @param s String to duplicate + * @return Newly allocated copy of the string, or NULL on failure + */ +JS_API char* js_strdup(const char* s); + +/* ============================================================================ + * Type Utility Functions + * ========================================================================== */ + +/** + * @brief Check if a type is a primitive (non-composite) type + * + * Primitive types are: null, boolean, integer, number, string, binary + * + * @param type Type to check + * @return true if the type is primitive + */ +JS_API bool js_type_is_primitive(js_type_t type); + +/** + * @brief Check if a type is numeric + * + * Numeric types include: integer, number, and all int/uint/float/decimal variants + * + * @param type Type to check + * @return true if the type is numeric + */ +JS_API bool js_type_is_numeric(js_type_t type); + +/** + * @brief Check if a type is a string-based type + * + * String types include: string and all format-specific string types + * (datetime, date, time, duration, uuid, uri, etc.) + * + * @param type Type to check + * @return true if the type is string-based + */ +JS_API bool js_type_is_string(js_type_t type); + +/** + * @brief Check if a type is an integer type + * + * Integer types include: integer, int8-int64, uint8-uint64 + * + * @param type Type to check + * @return true if the type is an integer type + */ +JS_API bool js_type_is_integer(js_type_t type); + +/** + * @brief Get the string name of a type + * + * @param type Type to get name for + * @return String name (e.g., "string", "int32", "datetime"), or "unknown" + */ +JS_API const char* js_type_name(js_type_t type); + +/** + * @brief Parse a type name string to its enum value + * + * @param name Type name string (case-sensitive) + * @return Corresponding js_type_t value, or JS_TYPE_UNKNOWN if not recognized + */ +JS_API js_type_t js_type_from_name(const char* name); + +/** + * @brief Get the type of a cJSON value + * + * Maps cJSON types to js_type_t. Note that this only returns basic JSON types + * (null, boolean, number, string, object, array) - not extended types. + * + * @param json cJSON value to check + * @return Basic JSON type, or JS_TYPE_UNKNOWN if NULL + */ +JS_API js_type_t js_type_of_json(const cJSON* json); + +/* ============================================================================ + * Error Functions + * ========================================================================== */ + +/** + * @brief Initialize an error structure + * + * @param error Error to initialize + */ +JS_API void js_error_init(js_error_t* error); + +/** + * @brief Clean up an error structure, freeing allocated memory + * + * @param error Error to clean up + */ +JS_API void js_error_cleanup(js_error_t* error); + +/** + * @brief Set error details + * + * @param error Error to set + * @param code Error code + * @param severity Error severity + * @param path JSON path (will be duplicated) + * @param message Error message (will be duplicated) + * @return true on success, false on allocation failure + */ +JS_API bool js_error_set(js_error_t* error, js_error_code_t code, js_severity_t severity, + const char* path, const char* message); + +/* ============================================================================ + * Result Functions + * ========================================================================== */ + +/** + * @brief Initialize a result structure + * + * Must be called before using a js_result_t. Sets valid to true + * and initializes error array. + * + * @param result Result to initialize + */ +JS_API void js_result_init(js_result_t* result); + +/** + * @brief Clean up a result structure, freeing all errors + * + * @param result Result to clean up + */ +JS_API void js_result_cleanup(js_result_t* result); + +/** + * @brief Add an error to a result + * + * Automatically grows the error array as needed, up to JS_MAX_ERRORS. + * Sets result->valid to false when adding an error with severity ERROR. + * + * @param result Result to add error to + * @param code Error code + * @param message Human-readable error message + * @param path JSON Pointer path to error location (may be NULL) + * @return true on success, false if max errors reached or allocation failed + */ +JS_API bool js_result_add_error(js_result_t* result, js_error_code_t code, + const char* message, const char* path); + +/** + * @brief Add a warning to a result + * + * Same as js_result_add_error but with SEVERITY_WARNING. + * Does not set result->valid to false. + * + * @param result Result to add warning to + * @param code Warning code + * @param message Human-readable warning message + * @param path JSON Pointer path to warning location (may be NULL) + * @return true on success, false if max errors reached or allocation failed + */ +JS_API bool js_result_add_warning(js_result_t* result, js_error_code_t code, + const char* message, const char* path); + +/** + * @brief Add an error with location information + * + * @param result Result to add error to + * @param code Error code + * @param message Human-readable error message + * @param path JSON Pointer path to error location (may be NULL) + * @param location Source location information + * @return true on success, false if max errors reached or allocation failed + */ +JS_API bool js_result_add_error_with_location(js_result_t* result, js_error_code_t code, + const char* message, const char* path, + js_location_t location); + +/** + * @brief Merge errors from one result into another + * + * @param dest Destination result + * @param src Source result (errors are copied, not moved) + * @return true on success, false if allocation failed + */ +JS_API bool js_result_merge(js_result_t* dest, const js_result_t* src); + +/** + * @brief Get a formatted string with all errors + * + * @param result Result to format + * @return Newly allocated string with all errors, or NULL on failure. + * Caller must free with js_free(). + */ +JS_API char* js_result_to_string(const js_result_t* result); + +/* ============================================================================ + * Location Functions + * ========================================================================== */ + +/** + * @brief Create a location structure + * + * @param line Line number (1-based) + * @param column Column number (1-based) + * @param offset Byte offset (0-based) + * @return Initialized location structure + */ +JS_API js_location_t js_location_make(int line, int column, size_t offset); + +/** + * @brief Check if a location is valid (has non-zero line) + * + * @param location Location to check + * @return true if location has valid line/column information + */ +JS_API bool js_location_is_valid(js_location_t location); + +#ifdef __cplusplus +} +#endif + +#endif /* JS_TYPES_H */ diff --git a/r/src/regex_utils.c b/r/src/regex_utils.c new file mode 100644 index 0000000..6228de6 --- /dev/null +++ b/r/src/regex_utils.c @@ -0,0 +1,980 @@ +/** + * @file regex_utils.c + * @brief Self-contained pure-C regular-expression matcher (ECMAScript subset). + * + * This is the R-package build of the JSON Structure regex utilities. The rest + * of the SDK compiles regex_utils.cpp (C++ std::regex), but that cannot be used + * inside an R package on Windows: MinGW/Rtools statically links libstdc++, and + * libstdc++'s lazy std::locale initialization (via __gthread_once) deadlocks the + * first time std::regex is constructed in a DLL loaded by R. CRAN mandates the + * static toolchain, so there is no link-flag workaround. + * + * To keep the package pure C (no libstdc++, no locale, identical behaviour on + * every platform), the three-function C ABI declared in regex_utils.h is + * reimplemented here with a small backtracking engine that supports the subset + * of ECMAScript regular expressions used by JSON Structure `pattern` keywords: + * + * - literals and the '.' wildcard (any byte except CR/LF) + * - anchors '^' and '$' (single-line; no /m multiline flag) + * - character classes '[...]' / '[^...]' with ranges and class escapes + * - shorthand classes \d \D \w \W \s \S and word boundaries \b \B + * - escapes \n \r \t \f \v \0 \xHH \uHHHH and escaped metacharacters + * - greedy and lazy quantifiers * + ? {n} {n,} {n,m} (and *? +? etc.) + * - grouping '(...)' and non-capturing '(?:...)', and alternation '|' + * + * Constructs that std::regex's ECMAScript grammar rejects (lookbehind, + * named groups, inline flags such as "(?i)", Unicode property escapes \p{...}) + * are reported as invalid patterns, matching the "throws => invalid" behaviour + * relied upon by the shared conformance corpus. Matching is byte-oriented, the + * same as std::regex over `char`. + * + * A step budget bounds backtracking so pathological "ReDoS" patterns fail fast + * instead of hanging. + * + * Copyright (c) 2024 JSON Structure Contributors + * SPDX-License-Identifier: MIT + */ + +#include "regex_utils.h" + +#include +#include + +/* ============================================================================ + * Character sets (256-bit bitmap over byte values) + * ========================================================================== */ + +typedef struct { + unsigned char bits[32]; +} CharSet; + +static void cs_zero(CharSet *cs) { memset(cs->bits, 0, sizeof(cs->bits)); } + +static void cs_set(CharSet *cs, int b) { + cs->bits[(b & 0xFF) >> 3] |= (unsigned char)(1u << (b & 7)); +} + +static int cs_get(const CharSet *cs, int b) { + return (cs->bits[(b & 0xFF) >> 3] >> (b & 7)) & 1; +} + +static void cs_add_range(CharSet *cs, int lo, int hi) { + int i; + if (lo < 0) lo = 0; + if (hi > 255) hi = 255; + for (i = lo; i <= hi; ++i) cs_set(cs, i); +} + +static void cs_invert(CharSet *cs) { + int i; + for (i = 0; i < 32; ++i) cs->bits[i] = (unsigned char)~cs->bits[i]; +} + +static int is_word_byte(int b) { + return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || + (b >= '0' && b <= '9') || b == '_'; +} + +/* Add a shorthand class (\d \D \w \W \s \S) to a set. Returns 1 on success. */ +static int cs_add_shorthand(CharSet *cs, char kind) { + CharSet tmp; + cs_zero(&tmp); + switch (kind) { + case 'd': cs_add_range(&tmp, '0', '9'); break; + case 'w': + cs_add_range(&tmp, 'a', 'z'); + cs_add_range(&tmp, 'A', 'Z'); + cs_add_range(&tmp, '0', '9'); + cs_set(&tmp, '_'); + break; + case 's': + cs_set(&tmp, ' '); + cs_set(&tmp, '\t'); + cs_set(&tmp, '\n'); + cs_set(&tmp, '\r'); + cs_set(&tmp, '\f'); + cs_set(&tmp, '\v'); + break; + case 'D': cs_add_range(&tmp, '0', '9'); cs_invert(&tmp); break; + case 'W': + cs_add_range(&tmp, 'a', 'z'); + cs_add_range(&tmp, 'A', 'Z'); + cs_add_range(&tmp, '0', '9'); + cs_set(&tmp, '_'); + cs_invert(&tmp); + break; + case 'S': + cs_set(&tmp, ' '); + cs_set(&tmp, '\t'); + cs_set(&tmp, '\n'); + cs_set(&tmp, '\r'); + cs_set(&tmp, '\f'); + cs_set(&tmp, '\v'); + cs_invert(&tmp); + break; + default: + return 0; + } + { + int i; + for (i = 0; i < 32; ++i) cs->bits[i] |= tmp.bits[i]; + } + return 1; +} + +/* ============================================================================ + * AST + * ========================================================================== */ + +enum { + A_EMPTY, A_LIT, A_ANY, A_SET, A_BOL, A_EOL, A_WORDB, A_CONCAT, A_ALT, A_REPEAT +}; + +typedef struct { + int type; + int c; /* A_LIT: byte value */ + int set; /* A_SET: index into Comp.sets */ + int neg; /* A_WORDB: 1 => \B */ + int min, max, greedy; /* A_REPEAT (max == -1 => unbounded) */ + int child; /* A_REPEAT: child node index */ + int *kids; /* A_CONCAT / A_ALT: child node indices */ + int nkids; +} Node; + +typedef struct { + const char *p; + int pos; + int len; + int error; + + Node *nodes; + int nn, capn; + + CharSet *sets; + int ns, caps; +} Comp; + +static int new_node(Comp *c, int type) { + if (c->error) return -1; + if (c->nn >= c->capn) { + int ncap = c->capn ? c->capn * 2 : 32; + Node *nn = (Node *)realloc(c->nodes, (size_t)ncap * sizeof(Node)); + if (!nn) { c->error = 1; return -1; } + c->nodes = nn; + c->capn = ncap; + } + { + Node *n = &c->nodes[c->nn]; + memset(n, 0, sizeof(*n)); + n->type = type; + n->min = n->max = 0; + n->greedy = 1; + n->child = -1; + return c->nn++; + } +} + +static void add_kid(Comp *c, int parent, int kid) { + Node *n; + if (c->error || parent < 0 || kid < 0) return; + n = &c->nodes[parent]; + { + int *nk = (int *)realloc(n->kids, (size_t)(n->nkids + 1) * sizeof(int)); + if (!nk) { c->error = 1; return; } + n->kids = nk; + n->kids[n->nkids++] = kid; + } +} + +static int new_set(Comp *c, const CharSet *cs) { + if (c->error) return -1; + if (c->ns >= c->caps) { + int ncap = c->caps ? c->caps * 2 : 16; + CharSet *nsp = (CharSet *)realloc(c->sets, (size_t)ncap * sizeof(CharSet)); + if (!nsp) { c->error = 1; return -1; } + c->sets = nsp; + c->caps = ncap; + } + c->sets[c->ns] = *cs; + return c->ns++; +} + +/* ---- parser ---- */ + +static int parse_alt(Comp *c); + +static int hexval(int ch) { + if (ch >= '0' && ch <= '9') return ch - '0'; + if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; + if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10; + return -1; +} + +/* Decode a backslash escape that resolves to a single byte value inside or + * outside a character class. On entry c->pos points just past the backslash; + * the escape letter is `e` and c->pos already advanced past it. Returns the + * byte value, or -1 if the escape is a shorthand class (caller handles those + * separately) — but this helper is only called for non-shorthand escapes. */ +static int decode_char_escape(Comp *c, int e, int in_class) { + switch (e) { + case 'n': return '\n'; + case 'r': return '\r'; + case 't': return '\t'; + case 'f': return '\f'; + case 'v': return '\v'; + case '0': return '\0'; + case 'b': return in_class ? 8 /* backspace */ : 'b'; + case 'x': { + int h1, h2; + if (c->pos + 1 < c->len && + (h1 = hexval((unsigned char)c->p[c->pos])) >= 0 && + (h2 = hexval((unsigned char)c->p[c->pos + 1])) >= 0) { + c->pos += 2; + return (h1 << 4) | h2; + } + return 'x'; + } + case 'u': { + int h1, h2, h3, h4; + if (c->pos + 3 < c->len && + (h1 = hexval((unsigned char)c->p[c->pos])) >= 0 && + (h2 = hexval((unsigned char)c->p[c->pos + 1])) >= 0 && + (h3 = hexval((unsigned char)c->p[c->pos + 2])) >= 0 && + (h4 = hexval((unsigned char)c->p[c->pos + 3])) >= 0) { + int cp = (h1 << 12) | (h2 << 8) | (h3 << 4) | h4; + c->pos += 4; + /* Byte-oriented engine: keep the low byte. Multi-byte code points + * are rare in `pattern` and not exercised by the corpus. */ + return cp & 0xFF; + } + return 'u'; + } + default: + /* Identity escape: \. \\ \* \+ \? \( \) \[ \] \{ \} \| \^ \$ \/ \- etc. + * Also lenient for other letters (\a => 'a'). */ + return (unsigned char)e; + } +} + +/* Parse a character class starting at '['. Returns an A_SET node or -1. */ +static int parse_class(Comp *c) { + CharSet cs; + int negate = 0; + cs_zero(&cs); + c->pos++; /* consume '[' */ + if (c->pos < c->len && c->p[c->pos] == '^') { + negate = 1; + c->pos++; + } + while (c->pos < c->len && c->p[c->pos] != ']') { + int lo; + int lo_is_shorthand = 0; + if (c->p[c->pos] == '\\') { + int e; + c->pos++; + if (c->pos >= c->len) { c->error = 1; return -1; } + e = (unsigned char)c->p[c->pos++]; + if (e == 'd' || e == 'D' || e == 'w' || e == 'W' || e == 's' || e == 'S') { + cs_add_shorthand(&cs, (char)e); + lo_is_shorthand = 1; + lo = -1; + } else { + lo = decode_char_escape(c, e, 1); + } + } else { + lo = (unsigned char)c->p[c->pos++]; + } + + if (lo_is_shorthand) { + continue; /* shorthand cannot be a range endpoint */ + } + + /* range? "lo - hi" where '-' is not immediately before ']' */ + if (c->pos + 1 < c->len && c->p[c->pos] == '-' && c->p[c->pos + 1] != ']') { + int hi; + c->pos++; /* consume '-' */ + if (c->p[c->pos] == '\\') { + int e; + c->pos++; + if (c->pos >= c->len) { c->error = 1; return -1; } + e = (unsigned char)c->p[c->pos++]; + if (e == 'd' || e == 'D' || e == 'w' || e == 'W' || + e == 's' || e == 'S') { + c->error = 1; /* range to shorthand is invalid */ + return -1; + } + hi = decode_char_escape(c, e, 1); + } else { + hi = (unsigned char)c->p[c->pos++]; + } + if (lo > hi) { c->error = 1; return -1; } + cs_add_range(&cs, lo, hi); + } else { + cs_set(&cs, lo); + } + } + if (c->pos >= c->len) { c->error = 1; return -1; } /* unterminated '[' */ + c->pos++; /* consume ']' */ + if (negate) cs_invert(&cs); + { + int idx = new_set(c, &cs); + int node = new_node(c, A_SET); + if (node < 0) return -1; + c->nodes[node].set = idx; + return node; + } +} + +static int parse_atom(Comp *c) { + int ch; + if (c->pos >= c->len) return new_node(c, A_EMPTY); + ch = (unsigned char)c->p[c->pos]; + switch (ch) { + case '(': { + int inner; + c->pos++; + if (c->pos < c->len && c->p[c->pos] == '?') { + /* Only "(?:" non-capturing groups are supported. Everything else + * ((?=, (?!, (?<, (?, (?i) ...) is reported as invalid. */ + if (c->pos + 1 < c->len && c->p[c->pos + 1] == ':') { + c->pos += 2; + } else { + c->error = 1; + return -1; + } + } + inner = parse_alt(c); + if (c->error) return -1; + if (c->pos >= c->len || c->p[c->pos] != ')') { c->error = 1; return -1; } + c->pos++; /* consume ')' */ + return inner; + } + case '[': + return parse_class(c); + case '.': + c->pos++; + return new_node(c, A_ANY); + case '^': + c->pos++; + return new_node(c, A_BOL); + case '$': + c->pos++; + return new_node(c, A_EOL); + case '*': + case '+': + case '?': + /* quantifier with nothing to repeat */ + c->error = 1; + return -1; + case '\\': { + int e; + c->pos++; + if (c->pos >= c->len) { c->error = 1; return -1; } /* dangling backslash */ + e = (unsigned char)c->p[c->pos++]; + if (e == 'd' || e == 'D' || e == 'w' || e == 'W' || e == 's' || e == 'S') { + CharSet cs; + int idx, node; + cs_zero(&cs); + cs_add_shorthand(&cs, (char)e); + idx = new_set(c, &cs); + node = new_node(c, A_SET); + if (node < 0) return -1; + c->nodes[node].set = idx; + return node; + } + if (e == 'b' || e == 'B') { + int node = new_node(c, A_WORDB); + if (node < 0) return -1; + c->nodes[node].neg = (e == 'B'); + return node; + } + if (e == 'p' || e == 'P') { + /* Unicode property escapes are not part of the ECMAScript grammar + * accepted by std::regex; treat as invalid for parity. */ + c->error = 1; + return -1; + } + { + int val = decode_char_escape(c, e, 0); + int node = new_node(c, A_LIT); + if (node < 0) return -1; + c->nodes[node].c = val; + return node; + } + } + default: { + int node = new_node(c, A_LIT); + c->pos++; + if (node < 0) return -1; + c->nodes[node].c = ch; + return node; + } + } +} + +/* Parse an optional quantifier following an atom. */ +static int parse_quant(Comp *c) { + int atom = parse_atom(c); + int min, max, greedy; + if (c->error || atom < 0) return atom; + if (c->pos >= c->len) return atom; + + switch (c->p[c->pos]) { + case '*': min = 0; max = -1; c->pos++; break; + case '+': min = 1; max = -1; c->pos++; break; + case '?': min = 0; max = 1; c->pos++; break; + case '{': { + int save = c->pos; + int n = 0, m, have = 0; + c->pos++; + while (c->pos < c->len && c->p[c->pos] >= '0' && c->p[c->pos] <= '9') { + n = n * 10 + (c->p[c->pos] - '0'); + if (n > 1000000000) n = 1000000000; + c->pos++; + have = 1; + } + if (!have) { c->pos = save; return atom; } /* literal '{' */ + min = n; + max = n; + if (c->pos < c->len && c->p[c->pos] == ',') { + c->pos++; + if (c->pos < c->len && c->p[c->pos] == '}') { + max = -1; /* {n,} */ + } else { + m = 0; + have = 0; + while (c->pos < c->len && c->p[c->pos] >= '0' && c->p[c->pos] <= '9') { + m = m * 10 + (c->p[c->pos] - '0'); + if (m > 1000000000) m = 1000000000; + c->pos++; + have = 1; + } + if (!have) { c->pos = save; return atom; } + max = m; + } + } + if (c->pos >= c->len || c->p[c->pos] != '}') { + c->pos = save; + return atom; /* not a valid quantifier: treat '{' as literal */ + } + c->pos++; /* consume '}' */ + if (max != -1 && max < min) { c->error = 1; return -1; } + break; + } + default: + return atom; + } + + greedy = 1; + if (c->pos < c->len && c->p[c->pos] == '?') { + greedy = 0; + c->pos++; + } + /* reject stacked quantifiers such as a**, a+*, a?+ */ + if (c->pos < c->len && + (c->p[c->pos] == '*' || c->p[c->pos] == '+' || c->p[c->pos] == '?')) { + c->error = 1; + return -1; + } + + { + int rep = new_node(c, A_REPEAT); + if (rep < 0) return -1; + c->nodes[rep].child = atom; + c->nodes[rep].min = min; + c->nodes[rep].max = max; + c->nodes[rep].greedy = greedy; + return rep; + } +} + +static int parse_concat(Comp *c) { + int node = new_node(c, A_CONCAT); + if (node < 0) return -1; + while (c->pos < c->len && c->p[c->pos] != '|' && c->p[c->pos] != ')') { + int a = parse_quant(c); + if (c->error) return -1; + add_kid(c, node, a); + if (c->error) return -1; + } + return node; +} + +static int parse_alt(Comp *c) { + int first = parse_concat(c); + if (c->error) return -1; + if (c->pos >= c->len || c->p[c->pos] != '|') return first; + { + int node = new_node(c, A_ALT); + if (node < 0) return -1; + add_kid(c, node, first); + while (c->pos < c->len && c->p[c->pos] == '|') { + int n; + c->pos++; + n = parse_concat(c); + if (c->error) return -1; + add_kid(c, node, n); + } + return node; + } +} + +/* ============================================================================ + * Program (backtracking bytecode) + * ========================================================================== */ + +enum { + OP_LIT, OP_ANY, OP_SET, OP_BOL, OP_EOL, OP_WORDB, OP_JMP, OP_SPLIT, OP_MATCH +}; + +typedef struct { + int op; + int c; /* OP_LIT */ + int set; /* OP_SET */ + int neg; /* OP_WORDB */ + int x, y;/* OP_JMP / OP_SPLIT targets */ +} Inst; + +struct js_regex_program { + Inst *inst; + int ninst, capinst; + CharSet *sets; + int nsets; + int anchored_bol; + int ok; +}; +typedef struct js_regex_program Program; + +static int emit(Program *pr, int op) { + if (!pr->ok) return -1; + if (pr->ninst >= pr->capinst) { + int ncap = pr->capinst ? pr->capinst * 2 : 64; + Inst *ni = (Inst *)realloc(pr->inst, (size_t)ncap * sizeof(Inst)); + if (!ni) { pr->ok = 0; return -1; } + pr->inst = ni; + pr->capinst = ncap; + } + { + Inst *in = &pr->inst[pr->ninst]; + memset(in, 0, sizeof(*in)); + in->op = op; + return pr->ninst++; + } +} + +/* Expansion cap: mandatory copies and finite optional ranges beyond this are + * approximated with an unbounded loop to keep the program small (only matters + * for pathological bounds like {1,1000000}). */ +#define REGEX_EXPAND_CAP 1000 + +static void emit_node(Comp *c, Program *pr, int idx); + +static void emit_star(Comp *c, Program *pr, int child, int greedy) { + int l1 = emit(pr, OP_SPLIT); + if (l1 < 0) return; + emit_node(c, pr, child); + { + int j = emit(pr, OP_JMP); + int l3; + if (j < 0) return; + pr->inst[j].x = l1; + l3 = pr->ninst; + if (greedy) { + pr->inst[l1].x = l1 + 1; + pr->inst[l1].y = l3; + } else { + pr->inst[l1].x = l3; + pr->inst[l1].y = l1 + 1; + } + } +} + +static void emit_repeat(Comp *c, Program *pr, int child, int min, int max, int greedy) { + int i; + int m = min; + if (m > REGEX_EXPAND_CAP) m = REGEX_EXPAND_CAP; + for (i = 0; i < m && pr->ok; ++i) emit_node(c, pr, child); + + if (max == -1) { + emit_star(c, pr, child, greedy); + return; + } + { + int opt = max - min; + if (opt < 0) opt = 0; + if (opt > REGEX_EXPAND_CAP) { + emit_star(c, pr, child, greedy); + return; + } + { + int *splits = NULL; + int nsplits = 0; + for (i = 0; i < opt && pr->ok; ++i) { + int sp = emit(pr, OP_SPLIT); + int *tmp; + if (sp < 0) break; + tmp = (int *)realloc(splits, (size_t)(nsplits + 1) * sizeof(int)); + if (!tmp) { pr->ok = 0; break; } + splits = tmp; + splits[nsplits++] = sp; + emit_node(c, pr, child); + } + { + int end = pr->ninst; + for (i = 0; i < nsplits; ++i) { + int sp = splits[i]; + if (greedy) { + pr->inst[sp].x = sp + 1; /* prefer taking the copy */ + pr->inst[sp].y = end; /* backtrack: stop early */ + } else { + pr->inst[sp].x = end; + pr->inst[sp].y = sp + 1; + } + } + } + free(splits); + } + } +} + +static void emit_node(Comp *c, Program *pr, int idx) { + Node *n; + if (!pr->ok || idx < 0) return; + n = &c->nodes[idx]; + switch (n->type) { + case A_EMPTY: + break; + case A_LIT: { + int k = emit(pr, OP_LIT); + if (k >= 0) pr->inst[k].c = n->c; + break; + } + case A_ANY: + emit(pr, OP_ANY); + break; + case A_SET: { + int k = emit(pr, OP_SET); + if (k >= 0) pr->inst[k].set = n->set; + break; + } + case A_BOL: + emit(pr, OP_BOL); + break; + case A_EOL: + emit(pr, OP_EOL); + break; + case A_WORDB: { + int k = emit(pr, OP_WORDB); + if (k >= 0) pr->inst[k].neg = n->neg; + break; + } + case A_CONCAT: { + int i; + for (i = 0; i < n->nkids && pr->ok; ++i) emit_node(c, pr, n->kids[i]); + break; + } + case A_ALT: { + /* k0 | k1 | ... | k_{m-1} */ + int m = n->nkids; + int i; + int *jmps = NULL; + int njmps = 0; + if (m == 0) break; + for (i = 0; i < m - 1 && pr->ok; ++i) { + int sp = emit(pr, OP_SPLIT); + int j, *tmp; + if (sp < 0) break; + pr->inst[sp].x = sp + 1; + emit_node(c, pr, n->kids[i]); + j = emit(pr, OP_JMP); + if (j < 0) break; + pr->inst[sp].y = pr->ninst; /* next alternative starts here */ + tmp = (int *)realloc(jmps, (size_t)(njmps + 1) * sizeof(int)); + if (!tmp) { pr->ok = 0; break; } + jmps = tmp; + jmps[njmps++] = j; + } + emit_node(c, pr, n->kids[m - 1]); + { + int end = pr->ninst; + for (i = 0; i < njmps; ++i) pr->inst[jmps[i]].x = end; + } + free(jmps); + break; + } + case A_REPEAT: + emit_repeat(c, pr, n->child, n->min, n->max, n->greedy); + break; + default: + break; + } +} + +static void comp_free(Comp *c) { + int i; + if (c->nodes) { + for (i = 0; i < c->nn; ++i) free(c->nodes[i].kids); + free(c->nodes); + } + free(c->sets); + c->nodes = NULL; + c->sets = NULL; +} + +static void program_free(Program *pr) { + if (!pr) return; + free(pr->inst); + free(pr->sets); + free(pr); +} + +/* Compile a pattern into a Program. Returns NULL on invalid/OOM. */ +static Program *program_compile(const char *pattern) { + Comp c; + Program *pr; + int root; + + memset(&c, 0, sizeof(c)); + c.p = pattern; + c.len = (int)strlen(pattern); + c.pos = 0; + + root = parse_alt(&c); + if (c.error || root < 0 || c.pos != c.len) { + comp_free(&c); + return NULL; + } + + pr = (Program *)calloc(1, sizeof(Program)); + if (!pr) { comp_free(&c); return NULL; } + pr->ok = 1; + + emit_node(&c, pr, root); + emit(pr, OP_MATCH); + + if (!pr->ok) { + comp_free(&c); + program_free(pr); + return NULL; + } + + /* transfer character sets to the program */ + if (c.ns > 0) { + pr->sets = (CharSet *)malloc((size_t)c.ns * sizeof(CharSet)); + if (!pr->sets) { comp_free(&c); program_free(pr); return NULL; } + memcpy(pr->sets, c.sets, (size_t)c.ns * sizeof(CharSet)); + pr->nsets = c.ns; + } + pr->anchored_bol = (pr->ninst > 0 && pr->inst[0].op == OP_BOL); + + comp_free(&c); + return pr; +} + +/* ============================================================================ + * Backtracking VM + * ========================================================================== */ + +#define REGEX_STEP_BUDGET 2000000L +#define REGEX_STACK_CAP 200000 + +typedef struct { int pc, sp; } Frame; + +static int word_boundary(const char *t, int len, int sp) { + int a = (sp > 0) ? is_word_byte((unsigned char)t[sp - 1]) : 0; + int b = (sp < len) ? is_word_byte((unsigned char)t[sp]) : 0; + return a != b; +} + +static int vm_run(const Program *pr, const char *t, int len, int start, long *budget) { + Frame *stack = (Frame *)malloc(sizeof(Frame) * 256); + int cap = 256, n = 0; + int result = 0; + if (!stack) return 0; + + stack[n].pc = 0; + stack[n].sp = start; + n++; + + while (n > 0) { + int pc, sp; + n--; + pc = stack[n].pc; + sp = stack[n].sp; + for (;;) { + const Inst *in; + if (--(*budget) < 0) { goto done; } + in = &pr->inst[pc]; + switch (in->op) { + case OP_LIT: + if (sp < len && (unsigned char)t[sp] == (unsigned char)in->c) { + pc++; sp++; continue; + } + goto backtrack; + case OP_ANY: + if (sp < len && t[sp] != '\n' && t[sp] != '\r') { pc++; sp++; continue; } + goto backtrack; + case OP_SET: + if (sp < len && cs_get(&pr->sets[in->set], (unsigned char)t[sp])) { + pc++; sp++; continue; + } + goto backtrack; + case OP_BOL: + if (sp == 0) { pc++; continue; } + goto backtrack; + case OP_EOL: + if (sp == len) { pc++; continue; } + goto backtrack; + case OP_WORDB: + if (word_boundary(t, len, sp) != in->neg) { pc++; continue; } + goto backtrack; + case OP_JMP: + pc = in->x; + continue; + case OP_SPLIT: + if (n >= cap) { + int ncap = cap * 2; + Frame *ns; + if (ncap > REGEX_STACK_CAP) { goto done; } + ns = (Frame *)realloc(stack, sizeof(Frame) * (size_t)ncap); + if (!ns) { goto done; } + stack = ns; + cap = ncap; + } + stack[n].pc = in->y; + stack[n].sp = sp; + n++; + pc = in->x; + continue; + case OP_MATCH: + result = 1; + goto done; + default: + goto done; + } + backtrack:; + break; /* pop next frame */ + } + } +done: + free(stack); + return result; +} + +static int program_search(const Program *pr, const char *text) { + int len = (int)strlen(text); + long budget = REGEX_STEP_BUDGET; + int start; + for (start = 0; start <= len; ++start) { + if (vm_run(pr, text, len, start, &budget)) return 1; + if (budget < 0) return 0; + if (pr->anchored_bol) break; /* '^' can match only at position 0 */ + } + return 0; +} + +/* ============================================================================ + * Compiled-program cache (single-threaded; R calls the binding on one thread) + * ========================================================================== */ + +#define REGEX_CACHE_SIZE 32 + +typedef struct { + char *pattern; + Program *program; /* NULL sentinel for a cached "invalid pattern" */ + int invalid; + unsigned long stamp; +} CacheEntry; + +static CacheEntry g_cache[REGEX_CACHE_SIZE]; +static unsigned long g_clock = 0; + +/* Returns the cached/compiled program for `pattern`, or NULL if invalid. + * `*out_invalid` distinguishes "invalid pattern" from "not looked up". */ +static Program *cache_lookup(const char *pattern, int *out_invalid) { + int i, victim = 0; + unsigned long oldest; + + *out_invalid = 0; + for (i = 0; i < REGEX_CACHE_SIZE; ++i) { + if (g_cache[i].pattern && strcmp(g_cache[i].pattern, pattern) == 0) { + g_cache[i].stamp = ++g_clock; + *out_invalid = g_cache[i].invalid; + return g_cache[i].program; + } + } + + /* not cached: compile */ + { + Program *pr = program_compile(pattern); + int invalid = (pr == NULL); + + /* choose an LRU victim (prefer an empty slot) */ + oldest = ~0UL; + for (i = 0; i < REGEX_CACHE_SIZE; ++i) { + if (!g_cache[i].pattern) { victim = i; break; } + if (g_cache[i].stamp < oldest) { oldest = g_cache[i].stamp; victim = i; } + } + if (g_cache[victim].pattern) { + free(g_cache[victim].pattern); + program_free(g_cache[victim].program); + g_cache[victim].pattern = NULL; + g_cache[victim].program = NULL; + } + { + size_t plen = strlen(pattern) + 1; + char *copy = (char *)malloc(plen); + if (copy) { + memcpy(copy, pattern, plen); + g_cache[victim].pattern = copy; + g_cache[victim].program = pr; + g_cache[victim].invalid = invalid; + g_cache[victim].stamp = ++g_clock; + } else { + /* cannot cache; still return the freshly compiled program. + * Leak-avoidance: without a cache slot we must free it, so the + * caller re-compiles next time. */ + program_free(pr); + pr = NULL; + if (!invalid) { *out_invalid = 0; return NULL; } + } + } + *out_invalid = invalid; + return pr; + } +} + +/* ============================================================================ + * Public C ABI (matches regex_utils.h) + * ========================================================================== */ + +bool js_regex_is_valid(const char *pattern) { + int invalid; + if (!pattern) return false; + (void)cache_lookup(pattern, &invalid); + return invalid ? false : true; +} + +bool js_regex_match(const char *pattern, const char *text) { + int invalid; + Program *pr; + if (!pattern || !text) return false; + pr = cache_lookup(pattern, &invalid); + if (!pr) return false; /* invalid pattern (or transient OOM) => no match */ + return program_search(pr, text) ? true : false; +} + +void js_regex_cache_clear(void) { + int i; + for (i = 0; i < REGEX_CACHE_SIZE; ++i) { + if (g_cache[i].pattern) { + free(g_cache[i].pattern); + program_free(g_cache[i].program); + g_cache[i].pattern = NULL; + g_cache[i].program = NULL; + g_cache[i].invalid = 0; + g_cache[i].stamp = 0; + } + } + g_clock = 0; +} diff --git a/r/src/regex_utils.h b/r/src/regex_utils.h new file mode 100644 index 0000000..ad1879c --- /dev/null +++ b/r/src/regex_utils.h @@ -0,0 +1,51 @@ +/** + * @file regex_utils.h + * @brief Simple regex utilities for pattern matching + * + * This module provides basic regex pattern validation and matching. + * On POSIX systems, it uses the standard regex.h. + * On Windows, it uses the CRT regex or a simple implementation. + * + * Copyright (c) 2024 JSON Structure Contributors + * SPDX-License-Identifier: MIT + */ + +#ifndef JS_REGEX_UTILS_H +#define JS_REGEX_UTILS_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Check if a regex pattern is valid + * + * @param pattern The regex pattern to validate + * @return true if the pattern compiles successfully, false otherwise + */ +bool js_regex_is_valid(const char* pattern); + +/** + * Match a string against a regex pattern + * + * @param pattern The regex pattern + * @param text The text to match + * @return true if text matches the pattern, false otherwise + */ +bool js_regex_match(const char* pattern, const char* text); + +/** + * Clear the regex compilation cache + * + * Frees all cached compiled regex objects. Useful for testing + * or when memory needs to be reclaimed. + */ +void js_regex_cache_clear(void); + +#ifdef __cplusplus +} +#endif + +#endif /* JS_REGEX_UTILS_H */ diff --git a/r/src/schema_validator.c b/r/src/schema_validator.c new file mode 100644 index 0000000..2844aeb --- /dev/null +++ b/r/src/schema_validator.c @@ -0,0 +1,2029 @@ +/** + * @file schema_validator.c + * @brief JSON Structure schema validator implementation + * + * Copyright (c) 2024 JSON Structure Contributors + * SPDX-License-Identifier: MIT + */ + +#include "json_structure/schema_validator.h" +#include "regex_utils.h" +#include +#include +#include +#include + +#ifndef JS_SCHEMA_EXTENSION_KEYWORD_NOT_ENABLED +#define JS_SCHEMA_EXTENSION_KEYWORD_NOT_ENABLED JS_SCHEMA_EXTENSION_KEYWORD_WITHOUT_USES +#endif + +/* ============================================================================ + * Internal Constants + * ============================================================================ */ + +#define MAX_DEPTH 100 +#define PATH_BUFFER_SIZE 1024 + +/* Valid primitive types */ +static const char* g_primitive_types[] = { + "null", "boolean", "integer", "number", "string", "binary", + "int8", "int16", "int32", "int64", + "uint8", "uint16", "uint32", "uint64", + "float16", "float32", "float64", "float128", "float", "double", + "decimal", "decimal64", "decimal128", + "datetime", "date", "time", "duration", + "uuid", "uri", "uri-reference", "uri-template", + "regex", "char", + "ipv4", "ipv6", + "email", "idn-email", "hostname", "idn-hostname", + "iri", "iri-reference", + "json-pointer", "relative-json-pointer", + "any", + NULL +}; + +/* Valid compound types */ +static const char* g_compound_types[] = { + "object", "array", "map", "set", "tuple", "choice", "abstract", + NULL +}; + +/* ============================================================================ + * Internal Types + * ============================================================================ */ + +#define MAX_IMPORT_DEPTH 16 + +typedef struct validate_context { + const js_schema_validator_t* validator; + js_result_t* result; + const cJSON* root_schema; + const cJSON* definitions; + char path[PATH_BUFFER_SIZE]; + int depth; +} validate_context_t; + +typedef struct import_context { + const js_schema_validator_t* validator; + js_result_t* result; + int import_depth; +} import_context_t; + +/* ============================================================================ + * Forward Declarations + * ============================================================================ */ + +static bool validate_schema_node(validate_context_t* ctx, const cJSON* schema); +static bool validate_object_properties(validate_context_t* ctx, const cJSON* schema); +static bool validate_array_items(validate_context_t* ctx, const cJSON* schema); +static bool validate_map_values(validate_context_t* ctx, const cJSON* schema); +static bool validate_choice_schema(validate_context_t* ctx, const cJSON* schema); +static bool validate_constraints(validate_context_t* ctx, const cJSON* schema, const char* type_name); +static bool validate_ucum_unit_keyword(validate_context_t* ctx, const cJSON* schema, const char* type_name); +static bool validate_units_extension_keywords(validate_context_t* ctx, const cJSON* schema, const char* type_name); +static bool validate_relations_keywords(validate_context_t* ctx, const cJSON* schema, const char* type_name); +static bool validate_extends_keyword(validate_context_t* ctx, const cJSON* extends_node); +static const cJSON* resolve_ref(validate_context_t* ctx, const char* ref); +static bool process_imports(import_context_t* ctx, cJSON* obj, const char* path); +static bool is_validation_extension_keyword(const char* keyword); +static void collect_validation_keyword_warnings(validate_context_t* ctx, const cJSON* node); + +/* ============================================================================ + * Helper Functions + * ============================================================================ */ + +static void push_path(validate_context_t* ctx, const char* segment) { + size_t len = strlen(ctx->path); + size_t seg_len = strlen(segment); + size_t needed = seg_len + (len > 0 && segment[0] != '[' ? 1 : 0); /* +1 for dot separator */ + + /* Check for buffer overflow - silently truncate if path too long */ + if (len + needed >= PATH_BUFFER_SIZE) { + return; /* Path too long, skip appending */ + } + + /* Buffer space was validated above, so the segment is guaranteed to + * fit; copy directly to avoid -Wformat-truncation false positives that + * arise from snprintf's runtime bound. */ + if (len == 0) { + memcpy(ctx->path, segment, seg_len + 1); + } else if (segment[0] == '[') { + memcpy(ctx->path + len, segment, seg_len + 1); + } else { + ctx->path[len] = '.'; + memcpy(ctx->path + len + 1, segment, seg_len + 1); + } +} + +static void pop_path(validate_context_t* ctx, size_t prev_len) { + ctx->path[prev_len] = '\0'; +} + +static void add_error(validate_context_t* ctx, js_error_code_t code, const char* message) { + js_result_add_error(ctx->result, code, message, ctx->path); +} + +static void add_warning(validate_context_t* ctx, js_error_code_t code, const char* message) { + js_result_add_warning(ctx->result, code, message, ctx->path); +} + +static bool is_string_in_list(const char* str, const char** list) { + if (!str) return false; + for (const char** p = list; *p != NULL; ++p) { + if (strcmp(str, *p) == 0) return true; + } + return false; +} + +static bool is_blank_string(const char* str) { + if (!str) return true; + + while (*str) { + if (!isspace((unsigned char)*str)) { + return false; + } + str++; + } + + return true; +} + +static bool has_uri_scheme(const char* str) { + if (!str || !isalpha((unsigned char)str[0])) { + return false; + } + + for (size_t i = 1; str[i] != '\0'; ++i) { + unsigned char ch = (unsigned char)str[i]; + if (ch == ':') { + return true; + } + if (!isalnum(ch) && ch != '+' && ch != '-' && ch != '.') { + return false; + } + } + + return false; +} + +static bool is_valid_identifier(const char* str) { + if (!str || str[0] == '\0') { + return false; + } + + unsigned char first = (unsigned char)str[0]; + if (!isalpha(first) && first != '_' && first != '$') { + return false; + } + + for (size_t i = 1; str[i] != '\0'; ++i) { + unsigned char ch = (unsigned char)str[i]; + if (!isalnum(ch) && ch != '_' && ch != '$') { + return false; + } + } + + return true; +} + +static bool is_numeric_enum_type(const char* type_name) { + static const char* numeric_types[] = { + "integer", "int8", "int16", "int32", "int64", + "uint8", "uint16", "uint32", "uint64", + "float", "double", "decimal", + NULL + }; + + return is_string_in_list(type_name, numeric_types); +} + +static bool is_enum_value_valid_for_type(const cJSON* value, const char* type_name) { + if (!type_name || is_string_in_list(type_name, g_compound_types)) { + return true; + } + + if (strcmp(type_name, "string") == 0) { + return cJSON_IsString(value); + } + if (is_numeric_enum_type(type_name)) { + return cJSON_IsNumber(value); + } + if (strcmp(type_name, "boolean") == 0) { + return cJSON_IsBool(value); + } + if (strcmp(type_name, "null") == 0) { + return cJSON_IsNull(value); + } + + return true; +} + +static bool has_enabled_extension(validate_context_t* ctx, const char* extension) { + if (!ctx || !ctx->root_schema || !extension) return false; + + const cJSON* uses = cJSON_GetObjectItemCaseSensitive(ctx->root_schema, "$uses"); + if (!uses || !cJSON_IsArray(uses)) return false; + + cJSON* item = NULL; + cJSON_ArrayForEach(item, uses) { + if (cJSON_IsString(item) && strcmp(item->valuestring, extension) == 0) { + return true; + } + } + + return false; +} + +static bool is_ucum_numeric_type(const char* type_name) { + static const char* ucum_numeric_types[] = { + "number", "integer", "float", "double", "decimal", + "int32", "uint32", "int64", "uint64", "int128", "uint128", + NULL + }; + + return is_string_in_list(type_name, ucum_numeric_types); +} + +static bool is_validation_extension_keyword(const char* keyword) { + static const char* validation_extension_keywords[] = { + "pattern", "format", "minLength", "maxLength", "minimum", "maximum", + "exclusiveMinimum", "exclusiveMaximum", "multipleOf", + "minItems", "maxItems", "uniqueItems", "contains", "minContains", "maxContains", + "minProperties", "maxProperties", "propertyNames", "patternProperties", "dependentRequired", + "minEntries", "maxEntries", "patternKeys", "keyNames", + "contentEncoding", "contentMediaType", "has", "default", + NULL + }; + + return is_string_in_list(keyword, validation_extension_keywords); +} + +static void collect_validation_keyword_warnings(validate_context_t* ctx, const cJSON* node) { + if (!ctx || !node) return; + + if (cJSON_IsObject(node)) { + cJSON* child = NULL; + cJSON_ArrayForEach(child, node) { + size_t prev_len = strlen(ctx->path); + if (child->string) { + push_path(ctx, child->string); + + if (is_validation_extension_keyword(child->string)) { + char msg[256]; + snprintf(msg, sizeof(msg), + "Validation extension keyword '%s' is used but validation extensions are not enabled. Add '\"$uses\": [\"JSONStructureValidation\"]' to enable validation, or this keyword will be ignored.", + child->string); + add_warning(ctx, JS_SCHEMA_EXTENSION_KEYWORD_NOT_ENABLED, msg); + } + } + + collect_validation_keyword_warnings(ctx, child); + pop_path(ctx, prev_len); + } + } else if (cJSON_IsArray(node)) { + cJSON* child = NULL; + int index = 0; + cJSON_ArrayForEach(child, node) { + size_t prev_len = strlen(ctx->path); + char segment[32]; + snprintf(segment, sizeof(segment), "[%d]", index++); + push_path(ctx, segment); + collect_validation_keyword_warnings(ctx, child); + pop_path(ctx, prev_len); + } + } +} + +static bool validate_relation_ref_object(validate_context_t* ctx, const cJSON* value, const char* keyword) { + bool valid = true; + size_t keyword_len = strlen(ctx->path); + push_path(ctx, keyword); + + if (!value || !cJSON_IsObject(value)) { + char msg[128]; + snprintf(msg, sizeof(msg), "'%s' must be an object with '$ref'.", keyword); + add_error(ctx, JS_SCHEMA_KEYWORD_INVALID_TYPE, msg); + pop_path(ctx, keyword_len); + return false; + } + + const cJSON* ref = cJSON_GetObjectItemCaseSensitive(value, "$ref"); + if (!ref || !cJSON_IsString(ref)) { + char msg[128]; + snprintf(msg, sizeof(msg), "'%s' must be an object with '$ref'.", keyword); + add_error(ctx, JS_SCHEMA_KEYWORD_INVALID_TYPE, msg); + pop_path(ctx, keyword_len); + return false; + } + + if (ref->valuestring && ref->valuestring[0] == '#') { + if (!resolve_ref(ctx, ref->valuestring)) { + size_t ref_len = strlen(ctx->path); + push_path(ctx, "$ref"); + char msg[256]; + snprintf(msg, sizeof(msg), "$ref '%s' not found", ref->valuestring); + add_error(ctx, JS_SCHEMA_REF_NOT_FOUND, msg); + pop_path(ctx, ref_len); + valid = false; + } + } + + pop_path(ctx, keyword_len); + return valid; +} + +/* ============================================================================ + * Type Validation + * ============================================================================ */ + +bool js_schema_is_valid_primitive_type(const char* type_name) { + return is_string_in_list(type_name, g_primitive_types); +} + +bool js_schema_is_valid_compound_type(const char* type_name) { + return is_string_in_list(type_name, g_compound_types); +} + +static bool is_valid_type_name(const char* type_name) { + return js_schema_is_valid_primitive_type(type_name) || + js_schema_is_valid_compound_type(type_name); +} + +/* ============================================================================ + * Reference Resolution + * ============================================================================ */ + +static const cJSON* resolve_ref(validate_context_t* ctx, const char* ref) { + if (!ref) return NULL; + + /* Handle internal references */ + if (ref[0] == '#') { + /* Primary format: #/definitions/Name */ + if (strncmp(ref, "#/definitions/", 14) == 0) { + const char* def_name = ref + 14; + if (ctx->definitions) { + return cJSON_GetObjectItemCaseSensitive(ctx->definitions, def_name); + } + } + /* Also support: #/$defs/Name (JSON Schema compatibility) */ + if (strncmp(ref, "#/$defs/", 8) == 0) { + const char* def_name = ref + 8; + if (ctx->definitions) { + return cJSON_GetObjectItemCaseSensitive(ctx->definitions, def_name); + } + } + /* Also handle older $definitions path */ + if (strncmp(ref, "#/$definitions/", 15) == 0) { + const char* def_name = ref + 15; + if (ctx->definitions) { + return cJSON_GetObjectItemCaseSensitive(ctx->definitions, def_name); + } + } + } + + return NULL; +} + +/* ============================================================================ + * Import Processing ($import and $importdefs) + * ============================================================================ */ + +/** + * @brief Fetch an external schema by URI from the import registry + */ +static cJSON* fetch_external_schema(const js_schema_validator_t* validator, const char* uri) { + if (!validator || !uri) return NULL; + + const js_import_registry_t* registry = validator->options.import_registry; + if (!registry || !registry->entries) return NULL; + + for (size_t i = 0; i < registry->count; i++) { + const js_import_entry_t* entry = ®istry->entries[i]; + if (!entry->uri) continue; + + if (strcmp(entry->uri, uri) == 0) { + if (entry->schema) { + char* json_str = cJSON_PrintUnformatted(entry->schema); + if (!json_str) return NULL; + cJSON* copy = cJSON_Parse(json_str); + free(json_str); + return copy; + } + + if (entry->file_path) { + FILE* f = fopen(entry->file_path, "rb"); + if (!f) return NULL; + + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + + if (size <= 0 || size > 10 * 1024 * 1024) { + fclose(f); + return NULL; + } + + char* content = (char*)malloc((size_t)size + 1); + if (!content) { + fclose(f); + return NULL; + } + + size_t read_size = fread(content, 1, (size_t)size, f); + fclose(f); + content[read_size] = '\0'; + + cJSON* schema = cJSON_Parse(content); + free(content); + return schema; + } + } + + if (entry->schema) { + const cJSON* id = cJSON_GetObjectItemCaseSensitive(entry->schema, "$id"); + if (id && cJSON_IsString(id) && strcmp(id->valuestring, uri) == 0) { + char* json_str = cJSON_PrintUnformatted(entry->schema); + if (!json_str) return NULL; + cJSON* copy = cJSON_Parse(json_str); + free(json_str); + return copy; + } + } + } + + return NULL; +} + +/** + * @brief Rewrite $ref pointers in imported content to point to new location + */ +static void rewrite_refs_schema(cJSON* obj, const char* target_path) { + if (!obj || !target_path) return; + + if (cJSON_IsObject(obj)) { + cJSON* item = NULL; + cJSON_ArrayForEach(item, obj) { + if (strcmp(item->string, "$ref") == 0 && cJSON_IsString(item)) { + const char* ref = item->valuestring; + if (ref && ref[0] == '#') { + const char* last_slash = strrchr(ref, '/'); + const char* ref_name = last_slash ? last_slash + 1 : ref + 1; + + if (ref_name && *ref_name) { + size_t new_len = strlen(target_path) + 1 + strlen(ref_name) + 1; + char* new_ref = (char*)malloc(new_len); + if (new_ref) { + snprintf(new_ref, new_len, "%s/%s", target_path, ref_name); + cJSON_SetValuestring(item, new_ref); + free(new_ref); + } + } + } + } else if (strcmp(item->string, "$extends") == 0) { + if (cJSON_IsString(item)) { + const char* ref = item->valuestring; + if (ref && ref[0] == '#') { + const char* last_slash = strrchr(ref, '/'); + const char* ref_name = last_slash ? last_slash + 1 : ref + 1; + if (ref_name && *ref_name) { + size_t new_len = strlen(target_path) + 1 + strlen(ref_name) + 1; + char* new_ref = (char*)malloc(new_len); + if (new_ref) { + snprintf(new_ref, new_len, "%s/%s", target_path, ref_name); + cJSON_SetValuestring(item, new_ref); + free(new_ref); + } + } + } + } else if (cJSON_IsArray(item)) { + cJSON* arr_item = NULL; + cJSON_ArrayForEach(arr_item, item) { + if (cJSON_IsString(arr_item)) { + const char* ref = arr_item->valuestring; + if (ref && ref[0] == '#') { + const char* last_slash = strrchr(ref, '/'); + const char* ref_name = last_slash ? last_slash + 1 : ref + 1; + if (ref_name && *ref_name) { + size_t new_len = strlen(target_path) + 1 + strlen(ref_name) + 1; + char* new_ref = (char*)malloc(new_len); + if (new_ref) { + snprintf(new_ref, new_len, "%s/%s", target_path, ref_name); + cJSON_SetValuestring(arr_item, new_ref); + free(new_ref); + } + } + } + } + } + } + } else { + rewrite_refs_schema(item, target_path); + } + } + } else if (cJSON_IsArray(obj)) { + cJSON* item = NULL; + cJSON_ArrayForEach(item, obj) { + rewrite_refs_schema(item, target_path); + } + } +} + +/** + * @brief Process $import and $importdefs keywords in schema + */ +static bool process_imports(import_context_t* ctx, cJSON* obj, const char* path) { + if (!obj || !cJSON_IsObject(obj)) return true; + + if (ctx->import_depth >= MAX_IMPORT_DEPTH) { + js_result_add_error(ctx->result, JS_SCHEMA_IMPORT_NOT_ALLOWED, + "Maximum import depth exceeded", path); + return false; + } + + const char* import_key = NULL; + cJSON* import_value = NULL; + + cJSON* import_item = cJSON_GetObjectItemCaseSensitive(obj, "$import"); + if (import_item) { + import_key = "$import"; + import_value = import_item; + } else { + import_item = cJSON_GetObjectItemCaseSensitive(obj, "$importdefs"); + if (import_item) { + import_key = "$importdefs"; + import_value = import_item; + } + } + + if (import_key && import_value) { + if (!ctx->validator->options.allow_import) { + char msg[256]; + snprintf(msg, sizeof(msg), "Import keyword '%s' encountered but allow_import not enabled", import_key); + js_result_add_error(ctx->result, JS_SCHEMA_IMPORT_NOT_ALLOWED, msg, path); + return false; + } + + if (!cJSON_IsString(import_value)) { + char msg[256]; + snprintf(msg, sizeof(msg), "Import keyword '%s' value must be a string URI", import_key); + js_result_add_error(ctx->result, JS_SCHEMA_IMPORT_NOT_ALLOWED, msg, path); + return false; + } + + const char* uri = import_value->valuestring; + + cJSON* external = fetch_external_schema(ctx->validator, uri); + if (!external) { + char msg[256]; + snprintf(msg, sizeof(msg), "Unable to fetch external schema from URI: %s", uri); + js_result_add_error(ctx->result, JS_SCHEMA_IMPORT_NOT_ALLOWED, msg, path); + return false; + } + + ctx->import_depth++; + bool import_result = process_imports(ctx, external, "#"); + ctx->import_depth--; + + if (!import_result) { + cJSON_Delete(external); + return false; + } + + cJSON* imported_defs = cJSON_CreateObject(); + if (!imported_defs) { + cJSON_Delete(external); + return false; + } + + if (strcmp(import_key, "$import") == 0) { + const cJSON* type_field = cJSON_GetObjectItemCaseSensitive(external, "type"); + const cJSON* name_field = cJSON_GetObjectItemCaseSensitive(external, "name"); + + if (type_field && name_field && cJSON_IsString(name_field)) { + char* ext_str = cJSON_PrintUnformatted(external); + if (ext_str) { + cJSON* ext_copy = cJSON_Parse(ext_str); + free(ext_str); + if (ext_copy) { + cJSON_AddItemToObject(imported_defs, name_field->valuestring, ext_copy); + } + } + } + + const cJSON* ext_defs = cJSON_GetObjectItemCaseSensitive(external, "definitions"); + if (!ext_defs) { + ext_defs = cJSON_GetObjectItemCaseSensitive(external, "$defs"); + } + if (ext_defs && cJSON_IsObject(ext_defs)) { + cJSON* def = NULL; + cJSON_ArrayForEach(def, ext_defs) { + if (def->string && !cJSON_GetObjectItemCaseSensitive(imported_defs, def->string)) { + char* def_str = cJSON_PrintUnformatted(def); + if (def_str) { + cJSON* def_copy = cJSON_Parse(def_str); + free(def_str); + if (def_copy) { + cJSON_AddItemToObject(imported_defs, def->string, def_copy); + } + } + } + } + } + } else { + const cJSON* ext_defs = cJSON_GetObjectItemCaseSensitive(external, "definitions"); + if (!ext_defs) { + ext_defs = cJSON_GetObjectItemCaseSensitive(external, "$defs"); + } + if (ext_defs && cJSON_IsObject(ext_defs)) { + cJSON* def = NULL; + cJSON_ArrayForEach(def, ext_defs) { + if (def->string) { + char* def_str = cJSON_PrintUnformatted(def); + if (def_str) { + cJSON* def_copy = cJSON_Parse(def_str); + free(def_str); + if (def_copy) { + cJSON_AddItemToObject(imported_defs, def->string, def_copy); + } + } + } + } + } + } + + const char* target_path; + cJSON* merge_target; + bool is_root = (strcmp(path, "#") == 0); + + if (is_root) { + target_path = "#/definitions"; + cJSON* defs = cJSON_GetObjectItemCaseSensitive(obj, "definitions"); + if (!defs) { + defs = cJSON_CreateObject(); + if (defs) { + cJSON_AddItemToObject(obj, "definitions", defs); + } + } + merge_target = defs; + } else { + target_path = path; + merge_target = obj; + } + + cJSON* def = NULL; + cJSON_ArrayForEach(def, imported_defs) { + rewrite_refs_schema(def, target_path); + } + + /* Merge imported definitions - collect keys first to avoid modifying during iteration */ + if (merge_target) { + int count = cJSON_GetArraySize(imported_defs); + if (count > 0) { + char** keys_to_merge = (char**)malloc(sizeof(char*) * (size_t)count); + if (keys_to_merge) { + int merge_count = 0; + cJSON* item = NULL; + cJSON_ArrayForEach(item, imported_defs) { + if (item->string && !cJSON_GetObjectItemCaseSensitive(merge_target, item->string)) { + keys_to_merge[merge_count++] = item->string; + } + } + + for (int i = 0; i < merge_count; i++) { + cJSON* detached = cJSON_DetachItemFromObject(imported_defs, keys_to_merge[i]); + if (detached) { + cJSON_AddItemToObject(merge_target, keys_to_merge[i], detached); + } + } + + free(keys_to_merge); + } + } + } + + cJSON_DeleteItemFromObject(obj, import_key); + + cJSON_Delete(imported_defs); + cJSON_Delete(external); + } + + cJSON* child = NULL; + cJSON_ArrayForEach(child, obj) { + if (child->string && strcmp(child->string, "properties") == 0) { + continue; + } + + if (cJSON_IsObject(child)) { + char child_path[PATH_BUFFER_SIZE]; + snprintf(child_path, sizeof(child_path), "%s/%s", path, child->string ? child->string : ""); + if (!process_imports(ctx, child, child_path)) { + return false; + } + } else if (cJSON_IsArray(child)) { + int idx = 0; + cJSON* arr_item = NULL; + cJSON_ArrayForEach(arr_item, child) { + if (cJSON_IsObject(arr_item)) { + char arr_path[PATH_BUFFER_SIZE]; + snprintf(arr_path, sizeof(arr_path), "%s/%s[%d]", path, child->string ? child->string : "", idx); + if (!process_imports(ctx, arr_item, arr_path)) { + return false; + } + } + idx++; + } + } + } + + return true; +} + +/* ============================================================================ + * Constraint Validation + * ============================================================================ */ + +static bool validate_string_constraints(validate_context_t* ctx, const cJSON* schema) { + bool valid = true; + const cJSON* minLength = cJSON_GetObjectItemCaseSensitive(schema, "minLength"); + const cJSON* maxLength = cJSON_GetObjectItemCaseSensitive(schema, "maxLength"); + const cJSON* pattern = cJSON_GetObjectItemCaseSensitive(schema, "pattern"); + + if (minLength) { + if (!cJSON_IsNumber(minLength)) { + add_error(ctx, JS_SCHEMA_MINLENGTH_INVALID, "minLength must be a number"); + valid = false; + } else if (minLength->valuedouble < 0) { + add_error(ctx, JS_SCHEMA_MINLENGTH_NEGATIVE, "minLength cannot be negative"); + valid = false; + } + } + + if (maxLength) { + if (!cJSON_IsNumber(maxLength)) { + add_error(ctx, JS_SCHEMA_MAXLENGTH_INVALID, "maxLength must be a number"); + valid = false; + } else if (maxLength->valuedouble < 0) { + add_error(ctx, JS_SCHEMA_MAXLENGTH_NEGATIVE, "maxLength cannot be negative"); + valid = false; + } + } + + if (minLength && maxLength && cJSON_IsNumber(minLength) && cJSON_IsNumber(maxLength)) { + if (minLength->valuedouble > maxLength->valuedouble) { + add_error(ctx, JS_SCHEMA_MINLENGTH_EXCEEDS_MAXLENGTH, "minLength exceeds maxLength"); + valid = false; + } + } + + if (pattern) { + if (!cJSON_IsString(pattern)) { + add_error(ctx, JS_SCHEMA_PATTERN_INVALID, "pattern must be a string"); + valid = false; + } else { + /* Validate regex syntax */ + if (!js_regex_is_valid(pattern->valuestring)) { + char msg[256]; + snprintf(msg, sizeof(msg), "Invalid regex pattern: %s", pattern->valuestring); + add_error(ctx, JS_SCHEMA_PATTERN_INVALID, msg); + valid = false; + } + } + } + + return valid; +} + +static bool validate_numeric_constraints(validate_context_t* ctx, const cJSON* schema) { + bool valid = true; + const cJSON* minimum = cJSON_GetObjectItemCaseSensitive(schema, "minimum"); + const cJSON* maximum = cJSON_GetObjectItemCaseSensitive(schema, "maximum"); + const cJSON* exclusiveMinimum = cJSON_GetObjectItemCaseSensitive(schema, "exclusiveMinimum"); + const cJSON* exclusiveMaximum = cJSON_GetObjectItemCaseSensitive(schema, "exclusiveMaximum"); + const cJSON* multipleOf = cJSON_GetObjectItemCaseSensitive(schema, "multipleOf"); + + if (minimum && !cJSON_IsNumber(minimum)) { + add_error(ctx, JS_SCHEMA_MIN_MAX_INVALID, "minimum must be a number"); + valid = false; + } + + if (maximum && !cJSON_IsNumber(maximum)) { + add_error(ctx, JS_SCHEMA_MIN_MAX_INVALID, "maximum must be a number"); + valid = false; + } + + if (exclusiveMinimum && !cJSON_IsNumber(exclusiveMinimum)) { + add_error(ctx, JS_SCHEMA_MIN_MAX_INVALID, "exclusiveMinimum must be a number"); + valid = false; + } + + if (exclusiveMaximum && !cJSON_IsNumber(exclusiveMaximum)) { + add_error(ctx, JS_SCHEMA_MIN_MAX_INVALID, "exclusiveMaximum must be a number"); + valid = false; + } + + if (minimum && maximum && cJSON_IsNumber(minimum) && cJSON_IsNumber(maximum)) { + if (minimum->valuedouble > maximum->valuedouble) { + add_error(ctx, JS_SCHEMA_MINIMUM_EXCEEDS_MAXIMUM, "minimum exceeds maximum"); + valid = false; + } + } + + if (multipleOf) { + if (!cJSON_IsNumber(multipleOf)) { + add_error(ctx, JS_SCHEMA_MULTIPLEOF_INVALID, "multipleOf must be a number"); + valid = false; + } else if (multipleOf->valuedouble <= 0) { + add_error(ctx, JS_SCHEMA_MULTIPLEOF_INVALID, "multipleOf must be positive"); + valid = false; + } + } + + return valid; +} + +static bool validate_array_constraints(validate_context_t* ctx, const cJSON* schema) { + bool valid = true; + const cJSON* minItems = cJSON_GetObjectItemCaseSensitive(schema, "minItems"); + const cJSON* maxItems = cJSON_GetObjectItemCaseSensitive(schema, "maxItems"); + + if (minItems) { + if (!cJSON_IsNumber(minItems)) { + add_error(ctx, JS_SCHEMA_MINITEMS_NEGATIVE, "minItems must be a number"); + valid = false; + } else if (minItems->valuedouble < 0) { + add_error(ctx, JS_SCHEMA_MINITEMS_NEGATIVE, "minItems cannot be negative"); + valid = false; + } + } + + if (maxItems) { + if (!cJSON_IsNumber(maxItems)) { + add_error(ctx, JS_SCHEMA_MIN_MAX_INVALID, "maxItems must be a number"); + valid = false; + } + } + + if (minItems && maxItems && cJSON_IsNumber(minItems) && cJSON_IsNumber(maxItems)) { + if (minItems->valuedouble > maxItems->valuedouble) { + add_error(ctx, JS_SCHEMA_MINITEMS_EXCEEDS_MAXITEMS, "minItems exceeds maxItems"); + valid = false; + } + } + + return valid; +} + +static bool validate_constraints(validate_context_t* ctx, const cJSON* schema, const char* type_name) { + bool valid = true; + bool is_string_type = (strcmp(type_name, "string") == 0 || js_type_is_string(js_type_from_name(type_name))); + bool is_numeric_type = (strcmp(type_name, "number") == 0 || strcmp(type_name, "integer") == 0 || + js_type_is_numeric(js_type_from_name(type_name))); + bool is_array_type = (strcmp(type_name, "array") == 0 || strcmp(type_name, "set") == 0); + + /* Check for constraint type mismatches */ + const cJSON* minimum = cJSON_GetObjectItemCaseSensitive(schema, "minimum"); + const cJSON* maximum = cJSON_GetObjectItemCaseSensitive(schema, "maximum"); + const cJSON* exclusiveMinimum = cJSON_GetObjectItemCaseSensitive(schema, "exclusiveMinimum"); + const cJSON* exclusiveMaximum = cJSON_GetObjectItemCaseSensitive(schema, "exclusiveMaximum"); + const cJSON* multipleOf = cJSON_GetObjectItemCaseSensitive(schema, "multipleOf"); + + if (!is_numeric_type && (minimum || maximum || exclusiveMinimum || exclusiveMaximum || multipleOf)) { + add_error(ctx, JS_SCHEMA_CONSTRAINT_TYPE_MISMATCH, + "Numeric constraints (minimum, maximum, etc.) can only be used with numeric types"); + valid = false; + } + + const cJSON* minLength = cJSON_GetObjectItemCaseSensitive(schema, "minLength"); + const cJSON* maxLength = cJSON_GetObjectItemCaseSensitive(schema, "maxLength"); + const cJSON* pattern = cJSON_GetObjectItemCaseSensitive(schema, "pattern"); + + if (!is_string_type && (minLength || maxLength || pattern)) { + add_error(ctx, JS_SCHEMA_CONSTRAINT_TYPE_MISMATCH, + "String constraints (minLength, maxLength, pattern) can only be used with string types"); + valid = false; + } + + const cJSON* minItems = cJSON_GetObjectItemCaseSensitive(schema, "minItems"); + const cJSON* maxItems = cJSON_GetObjectItemCaseSensitive(schema, "maxItems"); + + if (!is_array_type && (minItems || maxItems)) { + add_error(ctx, JS_SCHEMA_CONSTRAINT_TYPE_MISMATCH, + "Array constraints (minItems, maxItems) can only be used with array types"); + valid = false; + } + + /* String constraints */ + if (is_string_type) { + valid = validate_string_constraints(ctx, schema) && valid; + } + + /* Numeric constraints */ + if (is_numeric_type) { + valid = validate_numeric_constraints(ctx, schema) && valid; + } + + /* Array constraints */ + if (is_array_type) { + valid = validate_array_constraints(ctx, schema) && valid; + } + + return valid; +} + +static bool validate_ucum_unit_keyword(validate_context_t* ctx, const cJSON* schema, const char* type_name) { + const cJSON* ucum_unit = cJSON_GetObjectItemCaseSensitive(schema, "ucumUnit"); + if (!ucum_unit) return true; + + bool valid = true; + size_t prev_len = strlen(ctx->path); + push_path(ctx, "ucumUnit"); + + if (!has_enabled_extension(ctx, "JSONStructureUnits")) { + add_error(ctx, JS_SCHEMA_EXTENSION_KEYWORD_WITHOUT_USES, + "'ucumUnit' requires JSONStructureUnits extension."); + valid = false; + } + + if (!cJSON_IsString(ucum_unit)) { + add_error(ctx, JS_SCHEMA_KEYWORD_INVALID_TYPE, "'ucumUnit' must be a string."); + valid = false; + } + + if (!is_ucum_numeric_type(type_name)) { + add_error(ctx, JS_SCHEMA_CONSTRAINT_TYPE_MISMATCH, + "'ucumUnit' can only appear in numeric schemas."); + valid = false; + } + + pop_path(ctx, prev_len); + return valid; +} + +static bool validate_units_extension_keywords(validate_context_t* ctx, const cJSON* schema, const char* type_name) { + const cJSON* unit = cJSON_GetObjectItemCaseSensitive(schema, "unit"); + const cJSON* currency = cJSON_GetObjectItemCaseSensitive(schema, "currency"); + const cJSON* symbols = cJSON_GetObjectItemCaseSensitive(schema, "symbols"); + bool units_enabled = has_enabled_extension(ctx, "JSONStructureUnits"); + bool valid = true; + + if (unit) { + size_t prev_len = strlen(ctx->path); + push_path(ctx, "unit"); + + if (!units_enabled) { + add_error(ctx, JS_SCHEMA_EXTENSION_KEYWORD_WITHOUT_USES, + "'unit' requires JSONStructureUnits extension."); + valid = false; + } + if (!cJSON_IsString(unit)) { + add_error(ctx, JS_SCHEMA_KEYWORD_INVALID_TYPE, "'unit' must be a string."); + valid = false; + } + if (!is_ucum_numeric_type(type_name)) { + add_error(ctx, JS_SCHEMA_CONSTRAINT_TYPE_MISMATCH, + "'unit' can only appear in numeric schemas."); + valid = false; + } + + pop_path(ctx, prev_len); + } + + if (currency) { + size_t prev_len = strlen(ctx->path); + push_path(ctx, "currency"); + if (!units_enabled) { + add_error(ctx, JS_SCHEMA_EXTENSION_KEYWORD_WITHOUT_USES, + "'currency' requires JSONStructureUnits extension."); + valid = false; + } + pop_path(ctx, prev_len); + } + + if (symbols) { + size_t prev_len = strlen(ctx->path); + push_path(ctx, "symbols"); + if (!units_enabled) { + add_error(ctx, JS_SCHEMA_EXTENSION_KEYWORD_WITHOUT_USES, + "'symbols' requires JSONStructureUnits extension."); + valid = false; + } + pop_path(ctx, prev_len); + } + + return valid; +} + +static bool validate_relations_keywords(validate_context_t* ctx, const cJSON* schema, const char* type_name) { + const cJSON* identity = cJSON_GetObjectItemCaseSensitive(schema, "identity"); + const cJSON* relations = cJSON_GetObjectItemCaseSensitive(schema, "relations"); + if (!identity && !relations) return true; + + bool valid = true; + bool supports_relations = type_name && + (strcmp(type_name, "object") == 0 || strcmp(type_name, "tuple") == 0); + + if (!has_enabled_extension(ctx, "JSONStructureRelations")) { + if (identity) { + size_t prev_len = strlen(ctx->path); + push_path(ctx, "identity"); + add_error(ctx, JS_SCHEMA_EXTENSION_KEYWORD_WITHOUT_USES, + "'identity' requires JSONStructureRelations extension."); + pop_path(ctx, prev_len); + valid = false; + } + if (relations) { + size_t prev_len = strlen(ctx->path); + push_path(ctx, "relations"); + add_error(ctx, JS_SCHEMA_EXTENSION_KEYWORD_WITHOUT_USES, + "'relations' requires JSONStructureRelations extension."); + pop_path(ctx, prev_len); + valid = false; + } + } + + if (identity) { + size_t identity_len = strlen(ctx->path); + push_path(ctx, "identity"); + + if (!supports_relations) { + add_error(ctx, JS_SCHEMA_CONSTRAINT_TYPE_MISMATCH, + "'identity' can only appear in object or tuple schemas."); + valid = false; + } + + if (!cJSON_IsArray(identity)) { + add_error(ctx, JS_SCHEMA_KEYWORD_INVALID_TYPE, + "'identity' must be an array of strings."); + valid = false; + } else { + const cJSON* properties = cJSON_GetObjectItemCaseSensitive(schema, "properties"); + cJSON* item = NULL; + int index = 0; + cJSON_ArrayForEach(item, identity) { + char index_segment[32]; + snprintf(index_segment, sizeof(index_segment), "[%d]", index); + size_t item_len = strlen(ctx->path); + push_path(ctx, index_segment); + + if (!cJSON_IsString(item)) { + char msg[128]; + snprintf(msg, sizeof(msg), "'identity[%d]' must be a string.", index); + add_error(ctx, JS_SCHEMA_KEYWORD_INVALID_TYPE, msg); + valid = false; + } else if (!properties || !cJSON_IsObject(properties) || + !cJSON_GetObjectItemCaseSensitive(properties, item->valuestring)) { + char msg[256]; + snprintf(msg, sizeof(msg), "'identity' references property '%s' that is not in 'properties'.", + item->valuestring); + add_error(ctx, JS_SCHEMA_REQUIRED_PROPERTY_NOT_DEFINED, msg); + valid = false; + } + + pop_path(ctx, item_len); + index++; + } + } + + pop_path(ctx, identity_len); + } + + if (!relations) return valid; + + size_t relations_len = strlen(ctx->path); + push_path(ctx, "relations"); + + if (!supports_relations) { + add_error(ctx, JS_SCHEMA_CONSTRAINT_TYPE_MISMATCH, + "'relations' can only appear in object or tuple schemas."); + valid = false; + } + + if (!cJSON_IsObject(relations)) { + add_error(ctx, JS_SCHEMA_KEYWORD_INVALID_TYPE, "'relations' must be an object."); + pop_path(ctx, relations_len); + return false; + } + + cJSON* relation = NULL; + cJSON_ArrayForEach(relation, relations) { + size_t relation_len = strlen(ctx->path); + push_path(ctx, relation->string ? relation->string : ""); + + if (!cJSON_IsObject(relation)) { + add_error(ctx, JS_SCHEMA_KEYWORD_INVALID_TYPE, + "Relation declaration must be an object."); + valid = false; + pop_path(ctx, relation_len); + continue; + } + + const cJSON* targettype = cJSON_GetObjectItemCaseSensitive(relation, "targettype"); + if (!targettype) { + size_t target_len = strlen(ctx->path); + push_path(ctx, "targettype"); + add_error(ctx, JS_SCHEMA_KEYWORD_INVALID_TYPE, + "Relation declaration must have 'targettype'."); + pop_path(ctx, target_len); + valid = false; + } else if (!validate_relation_ref_object(ctx, targettype, "targettype")) { + valid = false; + } + + const cJSON* cardinality = cJSON_GetObjectItemCaseSensitive(relation, "cardinality"); + if (!cardinality) { + size_t card_len = strlen(ctx->path); + push_path(ctx, "cardinality"); + add_error(ctx, JS_SCHEMA_KEYWORD_INVALID_TYPE, + "Relation declaration must have 'cardinality'."); + pop_path(ctx, card_len); + valid = false; + } else { + size_t card_len = strlen(ctx->path); + push_path(ctx, "cardinality"); + if (!cJSON_IsString(cardinality) || + (strcmp(cardinality->valuestring, "single") != 0 && + strcmp(cardinality->valuestring, "multiple") != 0)) { + add_error(ctx, JS_SCHEMA_KEYWORD_INVALID_TYPE, + "'cardinality' must be 'single' or 'multiple'."); + valid = false; + } + pop_path(ctx, card_len); + } + + const cJSON* scope = cJSON_GetObjectItemCaseSensitive(relation, "scope"); + if (scope) { + size_t scope_len = strlen(ctx->path); + push_path(ctx, "scope"); + if (cJSON_IsString(scope)) { + /* valid */ + } else if (cJSON_IsArray(scope)) { + cJSON* scope_item = NULL; + int scope_index = 0; + cJSON_ArrayForEach(scope_item, scope) { + if (!cJSON_IsString(scope_item)) { + char index_segment[32]; + snprintf(index_segment, sizeof(index_segment), "[%d]", scope_index); + size_t item_len = strlen(ctx->path); + push_path(ctx, index_segment); + add_error(ctx, JS_SCHEMA_KEYWORD_INVALID_TYPE, + "'scope' array items must be strings."); + pop_path(ctx, item_len); + valid = false; + } + scope_index++; + } + } else { + add_error(ctx, JS_SCHEMA_KEYWORD_INVALID_TYPE, + "'scope' must be a string or an array of strings."); + valid = false; + } + pop_path(ctx, scope_len); + } + + const cJSON* qualifiertype = cJSON_GetObjectItemCaseSensitive(relation, "qualifiertype"); + if (qualifiertype && !validate_relation_ref_object(ctx, qualifiertype, "qualifiertype")) { + valid = false; + } + + pop_path(ctx, relation_len); + } + + pop_path(ctx, relations_len); + return valid; +} + +/* ============================================================================ + * Schema Node Validation + * ============================================================================ */ + +static bool validate_type_value(validate_context_t* ctx, const cJSON* type_node) { + if (cJSON_IsString(type_node)) { + const char* type_str = type_node->valuestring; + if (!is_valid_type_name(type_str)) { + char msg[256]; + snprintf(msg, sizeof(msg), "Unknown type: '%s'", type_str); + add_error(ctx, JS_SCHEMA_TYPE_INVALID, msg); + return false; + } + return true; + } + + if (cJSON_IsArray(type_node)) { + if (cJSON_GetArraySize(type_node) == 0) { + add_error(ctx, JS_SCHEMA_TYPE_ARRAY_EMPTY, "Type array cannot be empty"); + return false; + } + + bool valid = true; + cJSON* item; + cJSON_ArrayForEach(item, type_node) { + if (!cJSON_IsString(item)) { + add_error(ctx, JS_SCHEMA_TYPE_NOT_STRING, "Type array items must be strings"); + valid = false; + } else if (!is_valid_type_name(item->valuestring)) { + char msg[256]; + snprintf(msg, sizeof(msg), "Unknown type in array: '%s'", item->valuestring); + add_error(ctx, JS_SCHEMA_TYPE_INVALID, msg); + valid = false; + } + } + return valid; + } + + add_error(ctx, JS_SCHEMA_TYPE_NOT_STRING, "Type must be a string or array of strings"); + return false; +} + +static bool validate_definitions(validate_context_t* ctx, const cJSON* defs) { + if (!cJSON_IsObject(defs)) { + add_error(ctx, JS_SCHEMA_DEFINITIONS_MUST_BE_OBJECT, "definitions must be an object"); + return false; + } + + bool valid = true; + size_t prev_len = strlen(ctx->path); + push_path(ctx, "definitions"); + + cJSON* def; + cJSON_ArrayForEach(def, defs) { + size_t def_prev_len = strlen(ctx->path); + push_path(ctx, def->string); + + if (!validate_schema_node(ctx, def)) { + valid = false; + } + + pop_path(ctx, def_prev_len); + } + + pop_path(ctx, prev_len); + return valid; +} + +static bool validate_object_properties(validate_context_t* ctx, const cJSON* schema) { + const cJSON* properties = cJSON_GetObjectItemCaseSensitive(schema, "properties"); + const cJSON* required = cJSON_GetObjectItemCaseSensitive(schema, "required"); + const cJSON* additionalProperties = cJSON_GetObjectItemCaseSensitive(schema, "additionalProperties"); + + bool valid = true; + + if (properties) { + if (!cJSON_IsObject(properties)) { + add_error(ctx, JS_SCHEMA_PROPERTIES_MUST_BE_OBJECT, "properties must be an object"); + valid = false; + } else { + size_t prev_len = strlen(ctx->path); + push_path(ctx, "properties"); + + cJSON* prop; + cJSON_ArrayForEach(prop, properties) { + size_t prop_prev_len = strlen(ctx->path); + push_path(ctx, prop->string); + + if (!validate_schema_node(ctx, prop)) { + valid = false; + } + + pop_path(ctx, prop_prev_len); + } + + pop_path(ctx, prev_len); + } + } + + if (required) { + if (!cJSON_IsArray(required)) { + add_error(ctx, JS_SCHEMA_REQUIRED_MUST_BE_ARRAY, "required must be an array"); + valid = false; + } else { + cJSON* req_item; + cJSON_ArrayForEach(req_item, required) { + if (!cJSON_IsString(req_item)) { + add_error(ctx, JS_SCHEMA_REQUIRED_ITEM_MUST_BE_STRING, + "required items must be strings"); + valid = false; + } else if (properties && cJSON_IsObject(properties)) { + if (!cJSON_GetObjectItemCaseSensitive(properties, req_item->valuestring)) { + char msg[256]; + snprintf(msg, sizeof(msg), "Required property '%s' not defined in properties", + req_item->valuestring); + add_error(ctx, JS_SCHEMA_REQUIRED_PROPERTY_NOT_DEFINED, msg); + valid = false; + } + } + } + } + } + + if (additionalProperties) { + if (!cJSON_IsBool(additionalProperties) && !cJSON_IsObject(additionalProperties)) { + add_error(ctx, JS_SCHEMA_ADDITIONAL_PROPERTIES_INVALID, + "additionalProperties must be a boolean or schema"); + valid = false; + } else if (cJSON_IsObject(additionalProperties)) { + size_t prev_len = strlen(ctx->path); + push_path(ctx, "additionalProperties"); + if (!validate_schema_node(ctx, additionalProperties)) { + valid = false; + } + pop_path(ctx, prev_len); + } + } + + return valid; +} + +static bool validate_array_items(validate_context_t* ctx, const cJSON* schema) { + const cJSON* items = cJSON_GetObjectItemCaseSensitive(schema, "items"); + + if (!items) { + add_error(ctx, JS_SCHEMA_ARRAY_MISSING_ITEMS, "Array type requires items definition"); + return false; + } + + size_t prev_len = strlen(ctx->path); + push_path(ctx, "items"); + bool valid = validate_schema_node(ctx, items); + pop_path(ctx, prev_len); + + return valid; +} + +static bool validate_map_values(validate_context_t* ctx, const cJSON* schema) { + const cJSON* values = cJSON_GetObjectItemCaseSensitive(schema, "values"); + + if (!values) { + add_error(ctx, JS_SCHEMA_MAP_MISSING_VALUES, "Map type requires values definition"); + return false; + } + + size_t prev_len = strlen(ctx->path); + push_path(ctx, "values"); + bool valid = validate_schema_node(ctx, values); + pop_path(ctx, prev_len); + + return valid; +} + +static bool validate_tuple_schema(validate_context_t* ctx, const cJSON* schema) { + const cJSON* tuple_arr = cJSON_GetObjectItemCaseSensitive(schema, "tuple"); + const cJSON* properties = cJSON_GetObjectItemCaseSensitive(schema, "properties"); + + bool valid = true; + + /* tuple type requires tuple keyword */ + if (!tuple_arr) { + add_error(ctx, JS_SCHEMA_TUPLE_MISSING_DEFINITION, "Tuple type requires tuple keyword"); + valid = false; + } else if (!cJSON_IsArray(tuple_arr)) { + add_error(ctx, JS_SCHEMA_TUPLE_INVALID_FORMAT, "tuple must be an array"); + valid = false; + } + + /* tuple type requires properties keyword */ + if (!properties) { + add_error(ctx, JS_SCHEMA_TUPLE_MISSING_PROPERTIES, "Tuple type requires properties"); + valid = false; + } else if (!cJSON_IsObject(properties)) { + add_error(ctx, JS_SCHEMA_PROPERTIES_MUST_BE_OBJECT, "properties must be an object"); + valid = false; + } + + /* If both are present and valid, validate that tuple references properties */ + if (tuple_arr && cJSON_IsArray(tuple_arr) && properties && cJSON_IsObject(properties)) { + cJSON* tuple_item; + int tuple_index = 0; + cJSON_ArrayForEach(tuple_item, tuple_arr) { + size_t item_prev_len = strlen(ctx->path); + char index_segment[32]; + snprintf(index_segment, sizeof(index_segment), "[%d]", tuple_index++); + push_path(ctx, index_segment); + + if (cJSON_IsString(tuple_item)) { + if (!cJSON_GetObjectItemCaseSensitive(properties, tuple_item->valuestring)) { + char msg[256]; + snprintf(msg, sizeof(msg), "tuple references undefined property '%s'", + tuple_item->valuestring); + add_error(ctx, JS_SCHEMA_TUPLE_PROPERTY_NOT_DEFINED, msg); + valid = false; + } + } else if (cJSON_IsObject(tuple_item)) { + const cJSON* ref = cJSON_GetObjectItemCaseSensitive(tuple_item, "$ref"); + if (!ref || !cJSON_IsString(ref)) { + add_error(ctx, JS_SCHEMA_TUPLE_INVALID_FORMAT, "tuple $ref entries must contain a string $ref"); + valid = false; + } else if (!resolve_ref(ctx, ref->valuestring)) { + char msg[256]; + snprintf(msg, sizeof(msg), "$ref '%s' not found", ref->valuestring); + add_error(ctx, JS_SCHEMA_REF_NOT_FOUND, msg); + valid = false; + } + } else { + add_error(ctx, JS_SCHEMA_TUPLE_INVALID_FORMAT, "tuple array items must be strings or $ref objects"); + valid = false; + } + + pop_path(ctx, item_prev_len); + } + + /* Validate the properties themselves */ + size_t prev_len = strlen(ctx->path); + push_path(ctx, "properties"); + + cJSON* prop; + cJSON_ArrayForEach(prop, properties) { + size_t prop_prev_len = strlen(ctx->path); + push_path(ctx, prop->string); + + if (!validate_schema_node(ctx, prop)) { + valid = false; + } + + pop_path(ctx, prop_prev_len); + } + + pop_path(ctx, prev_len); + } + + return valid; +} + +static bool validate_choice_schema(validate_context_t* ctx, const cJSON* schema) { + const cJSON* choices = cJSON_GetObjectItemCaseSensitive(schema, "choices"); + const cJSON* selector = cJSON_GetObjectItemCaseSensitive(schema, "selector"); + + bool valid = true; + + if (!choices) { + add_error(ctx, JS_SCHEMA_CHOICE_MISSING_CHOICES, "Choice type requires choices definition"); + return false; + } + + if (!cJSON_IsObject(choices)) { + add_error(ctx, JS_SCHEMA_CHOICES_NOT_OBJECT, "choices must be an object"); + return false; + } + + if (selector && !cJSON_IsString(selector)) { + add_error(ctx, JS_SCHEMA_SELECTOR_NOT_STRING, "selector must be a string"); + valid = false; + } + + size_t prev_len = strlen(ctx->path); + push_path(ctx, "choices"); + + cJSON* choice; + cJSON_ArrayForEach(choice, choices) { + size_t choice_prev_len = strlen(ctx->path); + push_path(ctx, choice->string); + + if (!validate_schema_node(ctx, choice)) { + valid = false; + } + + pop_path(ctx, choice_prev_len); + } + + pop_path(ctx, prev_len); + return valid; +} + +static bool validate_enum(validate_context_t* ctx, const cJSON* enum_node, const char* type_name) { + if (!cJSON_IsArray(enum_node)) { + add_error(ctx, JS_SCHEMA_ENUM_NOT_ARRAY, "enum must be an array"); + return false; + } + + int size = cJSON_GetArraySize(enum_node); + if (size == 0) { + add_error(ctx, JS_SCHEMA_ENUM_EMPTY, "enum cannot be empty"); + return false; + } + + bool valid = true; + + /* Check for duplicates */ + for (int i = 0; i < size; i++) { + const cJSON* a = cJSON_GetArrayItem(enum_node, i); + for (int j = i + 1; j < size; j++) { + const cJSON* b = cJSON_GetArrayItem(enum_node, j); + if (cJSON_Compare(a, b, true)) { + add_error(ctx, JS_SCHEMA_ENUM_DUPLICATES, "enum contains duplicate values"); + return false; + } + } + } + + if (!type_name || js_schema_is_valid_compound_type(type_name)) { + return true; + } + + for (int i = 0; i < size; i++) { + const cJSON* value = cJSON_GetArrayItem(enum_node, i); + if (!is_enum_value_valid_for_type(value, type_name)) { + size_t prev_len = strlen(ctx->path); + char index_segment[32]; + snprintf(index_segment, sizeof(index_segment), "[%d]", i); + push_path(ctx, index_segment); + + char msg[256]; + snprintf(msg, sizeof(msg), "enum value is not valid for type '%s'", type_name); + add_error(ctx, JS_SCHEMA_CONSTRAINT_TYPE_MISMATCH, msg); + pop_path(ctx, prev_len); + valid = false; + } + } + + return valid; +} + +static bool validate_extends_keyword(validate_context_t* ctx, const cJSON* extends_node) { + bool valid = true; + + if (cJSON_IsString(extends_node)) { + if (!extends_node->valuestring || extends_node->valuestring[0] != '#') { + return true; + } + + const cJSON* target = resolve_ref(ctx, extends_node->valuestring); + if (!target) { + char msg[256]; + snprintf(msg, sizeof(msg), "$extends reference '%s' not found", extends_node->valuestring); + add_error(ctx, JS_SCHEMA_REF_NOT_FOUND, msg); + return false; + } + + const cJSON* type = cJSON_GetObjectItemCaseSensitive(target, "type"); + if (cJSON_IsString(type) && + strcmp(type->valuestring, "object") != 0 && strcmp(type->valuestring, "tuple") != 0 && + strcmp(type->valuestring, "map") != 0 && strcmp(type->valuestring, "array") != 0 && + strcmp(type->valuestring, "set") != 0 && strcmp(type->valuestring, "choice") != 0) { + char msg[256]; + snprintf(msg, sizeof(msg), "$extends target '%s' must not resolve to a primitive type", extends_node->valuestring); + add_error(ctx, JS_SCHEMA_CONSTRAINT_TYPE_MISMATCH, msg); + valid = false; + } + } else if (cJSON_IsArray(extends_node)) { + int index = 0; + cJSON* item = NULL; + cJSON_ArrayForEach(item, extends_node) { + size_t prev_len = strlen(ctx->path); + char index_segment[32]; + snprintf(index_segment, sizeof(index_segment), "[%d]", index++); + push_path(ctx, index_segment); + + if (!validate_extends_keyword(ctx, item)) { + valid = false; + } + + pop_path(ctx, prev_len); + } + } else { + add_error(ctx, JS_SCHEMA_KEYWORD_INVALID_TYPE, "$extends must be a string or array of strings"); + valid = false; + } + + return valid; +} + +static bool validate_composition(validate_context_t* ctx, const cJSON* schema) { + bool valid = true; + const cJSON* allOf = cJSON_GetObjectItemCaseSensitive(schema, "allOf"); + const cJSON* anyOf = cJSON_GetObjectItemCaseSensitive(schema, "anyOf"); + const cJSON* oneOf = cJSON_GetObjectItemCaseSensitive(schema, "oneOf"); + const cJSON* not_schema = cJSON_GetObjectItemCaseSensitive(schema, "not"); + const cJSON* if_schema = cJSON_GetObjectItemCaseSensitive(schema, "if"); + const cJSON* then_schema = cJSON_GetObjectItemCaseSensitive(schema, "then"); + const cJSON* else_schema = cJSON_GetObjectItemCaseSensitive(schema, "else"); + + if (allOf) { + if (!cJSON_IsArray(allOf)) { + add_error(ctx, JS_SCHEMA_ALLOF_NOT_ARRAY, "allOf must be an array"); + valid = false; + } else { + size_t prev_len = strlen(ctx->path); + push_path(ctx, "allOf"); + int idx = 0; + cJSON* item; + cJSON_ArrayForEach(item, allOf) { + char idx_str[32]; + snprintf(idx_str, sizeof(idx_str), "[%d]", idx); + size_t item_prev_len = strlen(ctx->path); + push_path(ctx, idx_str); + if (!validate_schema_node(ctx, item)) valid = false; + pop_path(ctx, item_prev_len); + idx++; + } + pop_path(ctx, prev_len); + } + } + + if (anyOf) { + if (!cJSON_IsArray(anyOf)) { + add_error(ctx, JS_SCHEMA_ANYOF_NOT_ARRAY, "anyOf must be an array"); + valid = false; + } else { + size_t prev_len = strlen(ctx->path); + push_path(ctx, "anyOf"); + int idx = 0; + cJSON* item; + cJSON_ArrayForEach(item, anyOf) { + char idx_str[32]; + snprintf(idx_str, sizeof(idx_str), "[%d]", idx); + size_t item_prev_len = strlen(ctx->path); + push_path(ctx, idx_str); + if (!validate_schema_node(ctx, item)) valid = false; + pop_path(ctx, item_prev_len); + idx++; + } + pop_path(ctx, prev_len); + } + } + + if (oneOf) { + if (!cJSON_IsArray(oneOf)) { + add_error(ctx, JS_SCHEMA_ONEOF_NOT_ARRAY, "oneOf must be an array"); + valid = false; + } else { + size_t prev_len = strlen(ctx->path); + push_path(ctx, "oneOf"); + int idx = 0; + cJSON* item; + cJSON_ArrayForEach(item, oneOf) { + char idx_str[32]; + snprintf(idx_str, sizeof(idx_str), "[%d]", idx); + size_t item_prev_len = strlen(ctx->path); + push_path(ctx, idx_str); + if (!validate_schema_node(ctx, item)) valid = false; + pop_path(ctx, item_prev_len); + idx++; + } + pop_path(ctx, prev_len); + } + } + + if (not_schema) { + size_t prev_len = strlen(ctx->path); + push_path(ctx, "not"); + if (!validate_schema_node(ctx, not_schema)) valid = false; + pop_path(ctx, prev_len); + } + + if (then_schema && !if_schema) { + add_error(ctx, JS_SCHEMA_THEN_WITHOUT_IF, "then requires if"); + valid = false; + } + + if (else_schema && !if_schema) { + add_error(ctx, JS_SCHEMA_ELSE_WITHOUT_IF, "else requires if"); + valid = false; + } + + if (if_schema) { + size_t prev_len = strlen(ctx->path); + push_path(ctx, "if"); + if (!validate_schema_node(ctx, if_schema)) valid = false; + pop_path(ctx, prev_len); + + if (then_schema) { + prev_len = strlen(ctx->path); + push_path(ctx, "then"); + if (!validate_schema_node(ctx, then_schema)) valid = false; + pop_path(ctx, prev_len); + } + + if (else_schema) { + prev_len = strlen(ctx->path); + push_path(ctx, "else"); + if (!validate_schema_node(ctx, else_schema)) valid = false; + pop_path(ctx, prev_len); + } + } + + return valid; +} + +static bool validate_schema_node(validate_context_t* ctx, const cJSON* schema) { + if (!schema) { + add_error(ctx, JS_SCHEMA_NULL, "Schema is null"); + return false; + } + + if (!cJSON_IsObject(schema)) { + add_error(ctx, JS_SCHEMA_INVALID_TYPE, "Schema must be an object"); + return false; + } + + ctx->depth++; + if (ctx->depth > MAX_DEPTH) { + add_error(ctx, JS_SCHEMA_MAX_DEPTH_EXCEEDED, "Maximum nesting depth exceeded"); + ctx->depth--; + return false; + } + + bool valid = true; + + /* Check for $ref */ + const cJSON* ref = cJSON_GetObjectItemCaseSensitive(schema, "$ref"); + if (ref) { + if (!cJSON_IsString(ref)) { + add_error(ctx, JS_SCHEMA_REF_NOT_STRING, "$ref must be a string"); + valid = false; + } else if (ref->valuestring[0] == '#' && !resolve_ref(ctx, ref->valuestring)) { + char msg[256]; + snprintf(msg, sizeof(msg), "$ref '%s' not found", ref->valuestring); + add_error(ctx, JS_SCHEMA_REF_NOT_FOUND, msg); + valid = false; + } + ctx->depth--; + return valid; + } + + /* Get type */ + const cJSON* type_node = cJSON_GetObjectItemCaseSensitive(schema, "type"); + const char* type_str = cJSON_IsString(type_node) ? type_node->valuestring : NULL; + if (type_node) { + if (!validate_type_value(ctx, type_node)) { + valid = false; + } + + if (type_str) { + /* Validate type-specific constraints */ + if (!validate_constraints(ctx, schema, type_str)) { + valid = false; + } + + /* Type-specific validation */ + if (strcmp(type_str, "object") == 0) { + if (!validate_object_properties(ctx, schema)) valid = false; + } else if (strcmp(type_str, "array") == 0 || strcmp(type_str, "set") == 0) { + if (!validate_array_items(ctx, schema)) valid = false; + } else if (strcmp(type_str, "map") == 0) { + if (!validate_map_values(ctx, schema)) valid = false; + } else if (strcmp(type_str, "tuple") == 0) { + if (!validate_tuple_schema(ctx, schema)) valid = false; + } else if (strcmp(type_str, "choice") == 0) { + if (!validate_choice_schema(ctx, schema)) valid = false; + } + } + + if (!validate_ucum_unit_keyword(ctx, schema, type_str)) { + valid = false; + } + if (!validate_units_extension_keywords(ctx, schema, type_str)) { + valid = false; + } + if (!validate_relations_keywords(ctx, schema, type_str)) { + valid = false; + } + } + + const cJSON* extends_node = cJSON_GetObjectItemCaseSensitive(schema, "$extends"); + if (extends_node) { + size_t prev_len = strlen(ctx->path); + push_path(ctx, "$extends"); + if (!validate_extends_keyword(ctx, extends_node)) { + valid = false; + } + pop_path(ctx, prev_len); + } + + /* Validate enum if present */ + const cJSON* enum_node = cJSON_GetObjectItemCaseSensitive(schema, "enum"); + if (enum_node) { + size_t prev_len = strlen(ctx->path); + push_path(ctx, "enum"); + if (!validate_enum(ctx, enum_node, type_str)) valid = false; + pop_path(ctx, prev_len); + } + + /* Validate definitions */ + const cJSON* defs = cJSON_GetObjectItemCaseSensitive(schema, "$defs"); + if (defs) { + if (!validate_definitions(ctx, defs)) valid = false; + } + + /* Legacy support for $definitions */ + const cJSON* definitions = cJSON_GetObjectItemCaseSensitive(schema, "$definitions"); + if (definitions && !defs) { + if (!validate_definitions(ctx, definitions)) valid = false; + } + + /* Legacy support for definitions (no $ prefix) */ + const cJSON* definitions_legacy = cJSON_GetObjectItemCaseSensitive(schema, "definitions"); + if (definitions_legacy && !defs && !definitions) { + if (!cJSON_IsObject(definitions_legacy)) { + add_error(ctx, JS_SCHEMA_DEFINITIONS_MUST_BE_OBJECT, "definitions must be an object"); + valid = false; + } else { + if (!validate_definitions(ctx, definitions_legacy)) valid = false; + } + } + + /* Validate composition keywords */ + if (!validate_composition(ctx, schema)) { + valid = false; + } + + ctx->depth--; + return valid; +} + +/* ============================================================================ + * Root Schema Validation + * ============================================================================ */ + +static bool validate_root_schema(validate_context_t* ctx, const cJSON* schema) { + bool valid = true; + + /* Check for required root properties */ + const cJSON* id = cJSON_GetObjectItemCaseSensitive(schema, "$id"); + const cJSON* schema_prop = cJSON_GetObjectItemCaseSensitive(schema, "$schema"); + const cJSON* name = cJSON_GetObjectItemCaseSensitive(schema, "name"); + const cJSON* type = cJSON_GetObjectItemCaseSensitive(schema, "type"); + + if (!id) { + add_warning(ctx, JS_SCHEMA_ROOT_MISSING_ID, "Root schema missing $id"); + } + + if (!schema_prop) { + add_warning(ctx, JS_SCHEMA_ROOT_MISSING_SCHEMA, "Root schema missing $schema"); + } + + if (!name) { + add_warning(ctx, JS_SCHEMA_ROOT_MISSING_NAME, "Root schema missing name"); + } + + if (id && cJSON_IsString(id)) { + size_t prev_len = strlen(ctx->path); + push_path(ctx, "$id"); + if (is_blank_string(id->valuestring)) { + add_error(ctx, JS_SCHEMA_KEYWORD_EMPTY, "$id must not be empty"); + valid = false; + } else if (!has_uri_scheme(id->valuestring)) { + add_error(ctx, JS_SCHEMA_CONSTRAINT_VALUE_INVALID, "$id must be a URI with a scheme"); + valid = false; + } + pop_path(ctx, prev_len); + } + + if (name && cJSON_IsString(name) && !is_valid_identifier(name->valuestring)) { + size_t prev_len = strlen(ctx->path); + push_path(ctx, "name"); + add_error(ctx, JS_SCHEMA_NAME_INVALID, "name must be a valid identifier"); + pop_path(ctx, prev_len); + valid = false; + } + + /* Check for composition keywords at root as alternative to type */ + const cJSON* root_ref = cJSON_GetObjectItemCaseSensitive(schema, "$root"); + const cJSON* allOf = cJSON_GetObjectItemCaseSensitive(schema, "allOf"); + const cJSON* anyOf = cJSON_GetObjectItemCaseSensitive(schema, "anyOf"); + const cJSON* oneOf = cJSON_GetObjectItemCaseSensitive(schema, "oneOf"); + const cJSON* not_schema = cJSON_GetObjectItemCaseSensitive(schema, "not"); + + bool has_type = type != NULL; + bool has_root = root_ref != NULL; + bool has_composition = allOf != NULL || anyOf != NULL || oneOf != NULL || not_schema != NULL; + + if (!has_type && !has_root && !has_composition) { + /* Type, $root, or composition keywords required for meaningful validation */ + add_error(ctx, JS_SCHEMA_ROOT_MISSING_TYPE, "Root schema must have 'type', '$root', or composition keywords"); + valid = false; + } + + /* Cache definitions for reference resolution - primary keyword is "definitions" */ + ctx->definitions = cJSON_GetObjectItemCaseSensitive(schema, "definitions"); + if (!ctx->definitions) { + ctx->definitions = cJSON_GetObjectItemCaseSensitive(schema, "$defs"); + } + if (!ctx->definitions) { + ctx->definitions = cJSON_GetObjectItemCaseSensitive(schema, "$definitions"); + } + + if (!has_enabled_extension(ctx, "JSONStructureValidation")) { + collect_validation_keyword_warnings(ctx, schema); + } + + /* Validate the schema tree */ + if (!validate_schema_node(ctx, schema)) { + valid = false; + } + + return valid; +} + +/* ============================================================================ + * Public API + * ============================================================================ */ + +void js_schema_validator_init(js_schema_validator_t* validator) { + if (validator) { + validator->options = JS_SCHEMA_OPTIONS_DEFAULT; + } +} + +void js_schema_validator_init_with_options(js_schema_validator_t* validator, + js_schema_options_t options) { + if (validator) { + validator->options = options; + } +} + +bool js_schema_validate(const js_schema_validator_t* validator, + const cJSON* schema, + js_result_t* result) { + if (!validator || !result) return false; + + js_result_init(result); + + if (!schema) { + js_result_add_error(result, JS_SCHEMA_NULL, "Schema is null", ""); + return false; + } + + /* Always process imports - either to resolve them (if allowed) or to detect errors */ + cJSON* schema_copy = NULL; + const cJSON* working_schema = schema; + + /* Create a mutable copy for import processing */ + char* schema_str = cJSON_PrintUnformatted(schema); + if (!schema_str) { + js_result_add_error(result, JS_SCHEMA_NULL, "Failed to process schema", ""); + return false; + } + schema_copy = cJSON_Parse(schema_str); + free(schema_str); + + if (!schema_copy) { + js_result_add_error(result, JS_SCHEMA_NULL, "Failed to copy schema", ""); + return false; + } + + import_context_t import_ctx = { + .validator = validator, + .result = result, + .import_depth = 0 + }; + + if (!process_imports(&import_ctx, schema_copy, "#")) { + cJSON_Delete(schema_copy); + return false; + } + + working_schema = schema_copy; + + validate_context_t ctx = { + .validator = validator, + .result = result, + .root_schema = working_schema, + .definitions = NULL, + .path = "", + .depth = 0 + }; + + bool valid = validate_root_schema(&ctx, working_schema); + + if (schema_copy) { + cJSON_Delete(schema_copy); + } + + return valid; +} + +bool js_schema_validate_string(const js_schema_validator_t* validator, + const char* json_str, + js_result_t* result) { + if (!validator || !result) return false; + + js_result_init(result); + + if (!json_str) { + js_result_add_error(result, JS_SCHEMA_NULL, "JSON string is null", ""); + return false; + } + + cJSON* schema = cJSON_Parse(json_str); + if (!schema) { + const char* error_ptr = cJSON_GetErrorPtr(); + char msg[256]; + if (error_ptr) { + snprintf(msg, sizeof(msg), "JSON parse error near: %.50s", error_ptr); + } else { + snprintf(msg, sizeof(msg), "JSON parse error"); + } + js_result_add_error(result, JS_SCHEMA_INVALID_TYPE, msg, ""); + return false; + } + + bool valid = js_schema_validate(validator, schema, result); + cJSON_Delete(schema); + return valid; +} + +/* Alias for C++ bindings */ +bool js_schema_validator_validate_string(const js_schema_validator_t* validator, + const char* json_str, + js_result_t* result) { + return js_schema_validate_string(validator, json_str, result); +} diff --git a/r/src/shim.c b/r/src/shim.c index 51eeb74..66aa841 100644 --- a/r/src/shim.c +++ b/r/src/shim.c @@ -1,14 +1,10 @@ /* - * shim.c - Thin R <-> JSON Structure C library binding. + * shim.c - Thin R <-> JSON Structure C engine binding. * - * Mirrors the Ruby SDK's ffi.rb: instead of statically linking the C - * validator, this shim loads a prebuilt "json_structure" shared library at - * runtime (downloaded from GitHub Releases with the user's consent, or pointed - * at by JSONSTRUCTURE_LIB_PATH) and resolves the small set of C entry points it - * needs. The struct ABI below is re-declared to match c/include/json_structure - * headers exactly, pinned to the same library version as this package. When the - * loaded engine exports a runtime version symbol, its ABI major version is - * checked for compatibility before use. + * The JSON Structure C engine (and its cJSON dependency) is compiled directly + * into this package's shared object, so there is nothing to load or download at + * runtime: the shim calls the engine entry points through the real public + * headers and marshals validation results back into R objects. * * SPDX-License-Identifier: MIT */ @@ -16,175 +12,14 @@ #include #include -#include -#include -#include -#include -#include #include -/* ------------------------------------------------------------------ */ -/* Re-declared C library ABI (must match the c/include/json_structure headers) */ -/* ------------------------------------------------------------------ */ - -typedef int js_error_code_t; /* typedef int in types.h */ - -typedef enum { /* js_severity_t */ - JS_SEVERITY_ERROR = 0, - JS_SEVERITY_WARNING = 1, - JS_SEVERITY_INFO = 2 -} js_severity_t; - -typedef struct { /* js_location_t */ - int line; - int column; - size_t offset; -} js_location_t; - -typedef struct { /* js_error_t */ - js_error_code_t code; - js_severity_t severity; - js_location_t location; - char *path; - char *message; -} js_error_t; - -typedef struct { /* js_result_t */ - bool valid; - js_error_t *errors; - size_t error_count; - size_t error_capacity; -} js_result_t; - -/* Only the size/layout of these matters; the C init functions fill them in. */ -typedef struct { /* js_schema_options_t */ - bool allow_import; - bool warnings_enabled; - const void *import_registry; -} js_schema_options_t; - -typedef struct { /* js_schema_validator_t */ - js_schema_options_t options; -} js_schema_validator_t; - -typedef struct { /* js_instance_options_t */ - bool allow_additional_properties; - bool validate_formats; - bool allow_import; - const void *import_registry; -} js_instance_options_t; - -typedef struct { /* js_instance_validator_t */ - js_instance_options_t options; -} js_instance_validator_t; - -/* Function pointer typedefs for the resolved entry points. */ -typedef void (*js_init_fn)(void); -typedef void (*js_cleanup_fn)(void); -typedef void (*js_result_init_fn)(js_result_t *); -typedef void (*js_result_cleanup_fn)(js_result_t *); -typedef void (*js_schema_validator_init_fn)(js_schema_validator_t *); -typedef bool (*js_schema_validate_string_fn)(const js_schema_validator_t *, - const char *, js_result_t *); -typedef void (*js_instance_validator_init_fn)(js_instance_validator_t *); -typedef bool (*js_instance_validate_strings_fn)(const js_instance_validator_t *, - const char *, const char *, - js_result_t *); - -/* - * Optional entry point exported by newer engines: - * const char *js_version_string(void); - * When present, its ABI major version is checked against the version this - * shim's struct layout was declared against. Absent in older builds. - */ -typedef const char *(*js_version_string_fn)(void); - -/* Engine ABI major version this shim is compatible with (pinned release). */ -#define JS_EXPECTED_ABI_MAJOR 0 - -/* ------------------------------------------------------------------ */ -/* Platform dynamic loading abstraction */ -/* ------------------------------------------------------------------ */ - -#ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN -#define NOGDI -#define NOMINMAX -#include -typedef HMODULE lib_handle_t; -/* - * The path arrives as UTF-8 (from Rf_translateCharUTF8). The ANSI - * LoadLibraryExA would misinterpret non-ASCII bytes (e.g. accented user names - * in the cache path), so convert to UTF-16 and use the wide entry point. The - * R_alloc scratch buffer is released when the enclosing .Call returns. - */ -static lib_handle_t lib_open(const char *p) { - int wlen = MultiByteToWideChar(CP_UTF8, 0, p, -1, NULL, 0); - if (wlen <= 0) { - return NULL; - } - wchar_t *wpath = (wchar_t *) R_alloc((size_t) wlen, sizeof(wchar_t)); - if (MultiByteToWideChar(CP_UTF8, 0, p, -1, wpath, wlen) <= 0) { - return NULL; - } - /* - * Restrict the dependency search to the system directories and the folder - * containing the library itself, rather than the current working directory - * or the full PATH, to avoid DLL pre-loading ("planting") hazards. These - * flags require an absolute path and Windows 8+/KB2533623; if they are - * rejected, fall back to the legacy altered-search-path behaviour. - */ - HMODULE h = LoadLibraryExW(wpath, NULL, - LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | - LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR); - if (h == NULL) { - h = LoadLibraryExW(wpath, NULL, LOAD_WITH_ALTERED_SEARCH_PATH); - } - return h; -} -static void *lib_sym(lib_handle_t h, const char *n) { - return (void *) GetProcAddress(h, n); -} -static void lib_close(lib_handle_t h) { if (h) FreeLibrary(h); } -static void last_error(char *buf, size_t n) { - DWORD e = GetLastError(); - if (!FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, e, 0, buf, (DWORD) n, NULL)) { - snprintf(buf, n, "Windows error %lu", (unsigned long) e); - } -} -#else -#include -typedef void *lib_handle_t; -static lib_handle_t lib_open(const char *p) { - /* RTLD_LOCAL keeps the engine's symbols out of the global namespace, so it - * cannot collide with or be shadowed by symbols from other loaded objects. */ - return dlopen(p, RTLD_NOW | RTLD_LOCAL); -} -static void *lib_sym(lib_handle_t h, const char *n) { return dlsym(h, n); } -static void lib_close(lib_handle_t h) { if (h) dlclose(h); } -static void last_error(char *buf, size_t n) { - const char *e = dlerror(); - snprintf(buf, n, "%s", e ? e : "unknown dynamic loader error"); -} -#endif +#include "json_structure/json_structure.h" /* ------------------------------------------------------------------ */ -/* Resolved symbols and handle */ +/* UTF-8 argument helpers */ /* ------------------------------------------------------------------ */ -static lib_handle_t g_handle = NULL; -static js_init_fn g_init = NULL; -static js_cleanup_fn g_cleanup = NULL; -static js_result_init_fn g_result_init = NULL; -static js_result_cleanup_fn g_result_cleanup = NULL; -static js_schema_validator_init_fn g_schema_init = NULL; -static js_schema_validate_string_fn g_schema_validate = NULL; -static js_instance_validator_init_fn g_instance_init = NULL; -static js_instance_validate_strings_fn g_instance_validate = NULL; -static js_version_string_fn g_version_string = NULL; -static char g_lib_version[64] = {0}; - static const char *to_utf8(SEXP s, const char *what) { if (TYPEOF(s) != STRSXP || LENGTH(s) < 1 || STRING_ELT(s, 0) == NA_STRING) { Rf_error("%s must be a non-NA character string", what); @@ -206,98 +41,9 @@ static const char *dup_utf8(SEXP s, const char *what) { } /* ------------------------------------------------------------------ */ -/* .Call entry points */ +/* Result marshalling */ /* ------------------------------------------------------------------ */ -/* Returns "" on success, or an error message on failure. */ -SEXP r_load_library(SEXP path_sexp) { - const char *path = to_utf8(path_sexp, "library path"); - char errbuf[512]; - - if (g_handle != NULL) { - return Rf_mkString(""); - } - - lib_handle_t h = lib_open(path); - if (h == NULL) { - last_error(errbuf, sizeof errbuf); - return Rf_mkString(errbuf); - } - - g_result_init = (js_result_init_fn) lib_sym(h, "js_result_init"); - g_result_cleanup = (js_result_cleanup_fn) lib_sym(h, "js_result_cleanup"); - g_schema_init = (js_schema_validator_init_fn) lib_sym(h, "js_schema_validator_init"); - g_schema_validate = (js_schema_validate_string_fn) lib_sym(h, "js_schema_validate_string"); - g_instance_init = (js_instance_validator_init_fn) lib_sym(h, "js_instance_validator_init"); - g_instance_validate = (js_instance_validate_strings_fn) lib_sym(h, "js_instance_validate_strings"); - /* Optional lifecycle hooks. */ - g_init = (js_init_fn) lib_sym(h, "js_init"); - g_cleanup = (js_cleanup_fn) lib_sym(h, "js_cleanup"); - - if (!g_result_init || !g_result_cleanup || !g_schema_init || - !g_schema_validate || !g_instance_init || !g_instance_validate) { - lib_close(h); - g_result_init = NULL; g_result_cleanup = NULL; - g_schema_init = NULL; g_schema_validate = NULL; - g_instance_init = NULL; g_instance_validate = NULL; - g_init = NULL; g_cleanup = NULL; - g_version_string = NULL; g_lib_version[0] = '\0'; - return Rf_mkString("json_structure library is missing required symbols"); - } - - /* - * Optional runtime ABI/version check. Older engines do not export - * js_version_string(); when present, refuse to use a library whose ABI - * major version differs from the one this shim's struct layout matches. - */ - g_version_string = (js_version_string_fn) lib_sym(h, "js_version_string"); - g_lib_version[0] = '\0'; - if (g_version_string != NULL) { - const char *v = g_version_string(); - if (v != NULL) { - snprintf(g_lib_version, sizeof g_lib_version, "%s", v); - long major = strtol(v, NULL, 10); - if (major != JS_EXPECTED_ABI_MAJOR) { - lib_close(h); - g_result_init = NULL; g_result_cleanup = NULL; - g_schema_init = NULL; g_schema_validate = NULL; - g_instance_init = NULL; g_instance_validate = NULL; - g_init = NULL; g_cleanup = NULL; - g_version_string = NULL; g_lib_version[0] = '\0'; - snprintf(errbuf, sizeof errbuf, - "json_structure ABI major version %ld is incompatible " - "with this package (expected %d)", - major, JS_EXPECTED_ABI_MAJOR); - return Rf_mkString(errbuf); - } - } - } - - if (g_init) g_init(); - g_handle = h; - return Rf_mkString(""); -} - -SEXP r_unload_library(void) { - if (g_handle != NULL) { - if (g_cleanup) g_cleanup(); - lib_close(g_handle); - g_handle = NULL; - } - g_version_string = NULL; - g_lib_version[0] = '\0'; - return R_NilValue; -} - -SEXP r_binding_loaded(void) { - return Rf_ScalarLogical(g_handle != NULL); -} - -/* Version string reported by the loaded engine, or "" if none / not loaded. */ -SEXP r_binding_version(void) { - return Rf_mkString(g_handle != NULL ? g_lib_version : ""); -} - /* Marshal a js_result_t into a named list of columns. */ static SEXP make_result(const js_result_t *res) { R_xlen_t n = (R_xlen_t) res->error_count; @@ -359,7 +105,7 @@ static SEXP marshal_body(void *data) { } static void marshal_cleanup(void *data, Rboolean jump) { (void) jump; - g_result_cleanup((js_result_t *) data); + js_result_cleanup((js_result_t *) data); } static SEXP marshal_result(js_result_t *res) { SEXP cont = PROTECT(R_MakeUnwindCont()); @@ -368,35 +114,49 @@ static SEXP marshal_result(js_result_t *res) { return out; } +/* ------------------------------------------------------------------ */ +/* .Call entry points */ +/* ------------------------------------------------------------------ */ + SEXP r_validate_schema(SEXP schema_sexp) { - if (g_handle == NULL) Rf_error("json_structure library is not loaded"); const char *schema = dup_utf8(schema_sexp, "schema"); js_schema_validator_t validator; memset(&validator, 0, sizeof validator); - g_schema_init(&validator); + js_schema_validator_init(&validator); js_result_t res; memset(&res, 0, sizeof res); - g_result_init(&res); - g_schema_validate(&validator, schema, &res); + js_result_init(&res); + js_schema_validate_string(&validator, schema, &res); return marshal_result(&res); } SEXP r_validate_instance(SEXP instance_sexp, SEXP schema_sexp) { - if (g_handle == NULL) Rf_error("json_structure library is not loaded"); const char *instance = dup_utf8(instance_sexp, "instance"); const char *schema = dup_utf8(schema_sexp, "schema"); js_instance_validator_t validator; memset(&validator, 0, sizeof validator); - g_instance_init(&validator); + js_instance_validator_init(&validator); js_result_t res; memset(&res, 0, sizeof res); - g_result_init(&res); - g_instance_validate(&validator, instance, schema, &res); + js_result_init(&res); + js_instance_validate_strings(&validator, instance, schema, &res); return marshal_result(&res); } + +/* Version string of the JSON Structure C engine compiled into this package. */ +SEXP r_engine_version(void) { + return Rf_mkString(JSON_STRUCTURE_VERSION_STRING); +} + +/* Release engine-held resources (e.g. the compiled-regex cache). Called from + * the package .onUnload hook so a reload starts from a clean state. */ +SEXP r_engine_cleanup(void) { + js_cleanup(); + return R_NilValue; +} diff --git a/r/src/types.c b/r/src/types.c new file mode 100644 index 0000000..b1c1159 --- /dev/null +++ b/r/src/types.c @@ -0,0 +1,655 @@ +/** + * @file types.c + * @brief JSON Structure core types implementation + * + * Copyright (c) 2024 JSON Structure Contributors + * SPDX-License-Identifier: MIT + */ + +#include "json_structure/types.h" +#include +#include +#include + +/* Thread synchronization for allocator + * + * On Windows, we use SRWLOCK (Slim Reader/Writer Lock) which has these benefits: + * - No initialization required (zero-initialized by default) + * - No cleanup/destruction needed + * - Very lightweight and fast + * - Available on Windows Vista and later + * + * On Unix, we use pthread_mutex with pthread_once for thread-safe initialization. + */ +#if defined(_WIN32) +#include +typedef SRWLOCK js_mutex_t; +#define JS_MUTEX_STATIC_INIT SRWLOCK_INIT +#define JS_MUTEX_LOCK(m) AcquireSRWLockExclusive(m) +#define JS_MUTEX_UNLOCK(m) ReleaseSRWLockExclusive(m) +/* SRWLOCK needs no init or destroy */ +#define JS_MUTEX_INIT(m) ((void)0) +#define JS_MUTEX_DESTROY(m) ((void)0) +#else +#include +typedef pthread_mutex_t js_mutex_t; +typedef pthread_once_t js_once_t; +#define JS_ONCE_INIT PTHREAD_ONCE_INIT +#define JS_MUTEX_INIT(m) pthread_mutex_init(m, NULL) +#define JS_MUTEX_LOCK(m) pthread_mutex_lock(m) +#define JS_MUTEX_UNLOCK(m) pthread_mutex_unlock(m) +#define JS_MUTEX_DESTROY(m) pthread_mutex_destroy(m) +#endif + +/* ============================================================================ + * Default Allocator + * ============================================================================ */ + +static void* default_malloc(size_t size) { + return malloc(size); +} + +static void* default_realloc(void* ptr, size_t size) { + return realloc(ptr, size); +} + +static void default_free(void* ptr) { + free(ptr); +} + +/* Global allocator - initialized to default */ +static js_allocator_t g_allocator = { + default_malloc, + default_realloc, + default_free, + NULL +}; + +/* Mutex to protect allocator access + * On Windows, SRWLOCK is zero-initialized by default which equals SRWLOCK_INIT, + * so no explicit initialization needed. + * On Unix, we use pthread_once for thread-safe initialization. + */ +#if defined(_WIN32) +/* SRWLOCK is statically initialized to zero (SRWLOCK_INIT) automatically */ +static js_mutex_t g_allocator_mutex = SRWLOCK_INIT; +#else +static js_mutex_t g_allocator_mutex; +static js_once_t g_allocator_once = JS_ONCE_INIT; + +static void init_allocator_mutex_once(void) { + JS_MUTEX_INIT(&g_allocator_mutex); +} +#endif + +/* Ensure allocator mutex is initialized (thread-safe) */ +static void ensure_allocator_mutex_init(void) { +#if defined(_WIN32) + /* SRWLOCK is already initialized statically - nothing to do */ + (void)0; +#else + pthread_once(&g_allocator_once, init_allocator_mutex_once); +#endif +} + +/* Public functions for mutex lifecycle management */ +void js_init_allocator_mutex(void) { + ensure_allocator_mutex_init(); +} + +void js_destroy_allocator_mutex(void) { +#if defined(_WIN32) + /* SRWLOCK does not need destruction */ + (void)0; +#else + /* Note: We cannot safely reset g_allocator_once after destruction + * because pthread_once_t is designed for one-time initialization. + * After js_cleanup() is called, the library should not be used again + * without a program restart. */ + JS_MUTEX_DESTROY(&g_allocator_mutex); +#endif +} + +/* ============================================================================ + * Allocator Functions + * ============================================================================ */ + +void js_set_allocator(js_allocator_t alloc) { + ensure_allocator_mutex_init(); + + JS_MUTEX_LOCK(&g_allocator_mutex); + + if (alloc.malloc && alloc.free) { + g_allocator = alloc; + /* Configure cJSON to use our allocator */ + cJSON_Hooks hooks = { + .malloc_fn = alloc.malloc, + .free_fn = alloc.free + }; + cJSON_InitHooks(&hooks); + } else { + /* Reset to defaults */ + g_allocator.malloc = default_malloc; + g_allocator.realloc = default_realloc; + g_allocator.free = default_free; + g_allocator.user_data = NULL; + cJSON_InitHooks(NULL); + } + + JS_MUTEX_UNLOCK(&g_allocator_mutex); +} + +js_allocator_t js_get_allocator(void) { + /* Reading the allocator struct - on modern platforms this is atomic enough + * for the expected use case. The allocator should only be set during init. */ + return g_allocator; +} + +void* js_malloc(size_t size) { + /* Note: The allocator functions (malloc/free) are expected to be thread-safe. + * We only lock when changing the allocator itself. */ + return g_allocator.malloc(size); +} + +void* js_realloc(void* ptr, size_t size) { + if (g_allocator.realloc) { + return g_allocator.realloc(ptr, size); + } + /* No realloc provided - cannot safely reallocate without knowing original size. + * Return NULL to signal failure. Callers should ensure realloc is provided + * in custom allocators, or this path should not be reached with default allocator. */ + if (ptr) { + return NULL; /* Cannot safely reallocate existing memory */ + } + /* For NULL ptr, realloc acts like malloc */ + return g_allocator.malloc(size); +} + +void js_free(void* ptr) { + if (ptr) { + g_allocator.free(ptr); + } +} + +char* js_strdup(const char* s) { + if (!s) return NULL; + size_t len = strlen(s) + 1; + char* copy = (char*)js_malloc(len); + if (copy) { + memcpy(copy, s, len); + } + return copy; +} + +/* ============================================================================ + * Type Utilities + * ============================================================================ */ + +bool js_type_is_primitive(js_type_t type) { + switch (type) { + case JS_TYPE_NULL: + case JS_TYPE_BOOLEAN: + case JS_TYPE_INTEGER: + case JS_TYPE_NUMBER: + case JS_TYPE_STRING: + case JS_TYPE_BINARY: + case JS_TYPE_INT8: + case JS_TYPE_INT16: + case JS_TYPE_INT32: + case JS_TYPE_INT64: + case JS_TYPE_UINT8: + case JS_TYPE_UINT16: + case JS_TYPE_UINT32: + case JS_TYPE_UINT64: + case JS_TYPE_FLOAT16: + case JS_TYPE_FLOAT32: + case JS_TYPE_FLOAT64: + case JS_TYPE_FLOAT128: + case JS_TYPE_DECIMAL: + case JS_TYPE_DECIMAL64: + case JS_TYPE_DECIMAL128: + case JS_TYPE_DATETIME: + case JS_TYPE_DATE: + case JS_TYPE_TIME: + case JS_TYPE_DURATION: + case JS_TYPE_UUID: + case JS_TYPE_URI: + case JS_TYPE_URI_REFERENCE: + case JS_TYPE_URI_TEMPLATE: + case JS_TYPE_REGEX: + case JS_TYPE_CHAR: + case JS_TYPE_IPV4: + case JS_TYPE_IPV6: + case JS_TYPE_EMAIL: + case JS_TYPE_IDN_EMAIL: + case JS_TYPE_HOSTNAME: + case JS_TYPE_IDN_HOSTNAME: + case JS_TYPE_IRI: + case JS_TYPE_IRI_REFERENCE: + case JS_TYPE_JSON_POINTER: + case JS_TYPE_RELATIVE_JSON_POINTER: + return true; + default: + return false; + } +} + +bool js_type_is_numeric(js_type_t type) { + switch (type) { + case JS_TYPE_INTEGER: + case JS_TYPE_NUMBER: + case JS_TYPE_INT8: + case JS_TYPE_INT16: + case JS_TYPE_INT32: + case JS_TYPE_INT64: + case JS_TYPE_UINT8: + case JS_TYPE_UINT16: + case JS_TYPE_UINT32: + case JS_TYPE_UINT64: + case JS_TYPE_FLOAT16: + case JS_TYPE_FLOAT32: + case JS_TYPE_FLOAT64: + case JS_TYPE_FLOAT128: + case JS_TYPE_DECIMAL: + case JS_TYPE_DECIMAL64: + case JS_TYPE_DECIMAL128: + return true; + default: + return false; + } +} + +bool js_type_is_string(js_type_t type) { + switch (type) { + case JS_TYPE_STRING: + case JS_TYPE_BINARY: + case JS_TYPE_DATETIME: + case JS_TYPE_DATE: + case JS_TYPE_TIME: + case JS_TYPE_DURATION: + case JS_TYPE_UUID: + case JS_TYPE_URI: + case JS_TYPE_URI_REFERENCE: + case JS_TYPE_URI_TEMPLATE: + case JS_TYPE_REGEX: + case JS_TYPE_CHAR: + case JS_TYPE_IPV4: + case JS_TYPE_IPV6: + case JS_TYPE_EMAIL: + case JS_TYPE_IDN_EMAIL: + case JS_TYPE_HOSTNAME: + case JS_TYPE_IDN_HOSTNAME: + case JS_TYPE_IRI: + case JS_TYPE_IRI_REFERENCE: + case JS_TYPE_JSON_POINTER: + case JS_TYPE_RELATIVE_JSON_POINTER: + return true; + default: + return false; + } +} + +bool js_type_is_integer(js_type_t type) { + switch (type) { + case JS_TYPE_INTEGER: + case JS_TYPE_INT8: + case JS_TYPE_INT16: + case JS_TYPE_INT32: + case JS_TYPE_INT64: + case JS_TYPE_UINT8: + case JS_TYPE_UINT16: + case JS_TYPE_UINT32: + case JS_TYPE_UINT64: + return true; + default: + return false; + } +} + +/* Type name lookup table */ +static const struct { + const char* name; + js_type_t type; +} g_type_names[] = { + {"any", JS_TYPE_ANY}, + {"null", JS_TYPE_NULL}, + {"boolean", JS_TYPE_BOOLEAN}, + {"integer", JS_TYPE_INTEGER}, + {"number", JS_TYPE_NUMBER}, + {"string", JS_TYPE_STRING}, + {"binary", JS_TYPE_BINARY}, + {"object", JS_TYPE_OBJECT}, + {"array", JS_TYPE_ARRAY}, + {"map", JS_TYPE_MAP}, + {"set", JS_TYPE_SET}, + {"abstract", JS_TYPE_ABSTRACT}, + {"choice", JS_TYPE_CHOICE}, + {"tuple", JS_TYPE_TUPLE}, + {"int8", JS_TYPE_INT8}, + {"int16", JS_TYPE_INT16}, + {"int32", JS_TYPE_INT32}, + {"int64", JS_TYPE_INT64}, + {"uint8", JS_TYPE_UINT8}, + {"uint16", JS_TYPE_UINT16}, + {"uint32", JS_TYPE_UINT32}, + {"uint64", JS_TYPE_UINT64}, + {"float16", JS_TYPE_FLOAT16}, + {"float32", JS_TYPE_FLOAT32}, + {"float64", JS_TYPE_FLOAT64}, + {"float128", JS_TYPE_FLOAT128}, + {"float", JS_TYPE_FLOAT32}, + {"double", JS_TYPE_FLOAT64}, + {"decimal", JS_TYPE_DECIMAL}, + {"decimal64", JS_TYPE_DECIMAL64}, + {"decimal128", JS_TYPE_DECIMAL128}, + {"datetime", JS_TYPE_DATETIME}, + {"date", JS_TYPE_DATE}, + {"time", JS_TYPE_TIME}, + {"duration", JS_TYPE_DURATION}, + {"uuid", JS_TYPE_UUID}, + {"uri", JS_TYPE_URI}, + {"uri-reference", JS_TYPE_URI_REFERENCE}, + {"uri-template", JS_TYPE_URI_TEMPLATE}, + {"regex", JS_TYPE_REGEX}, + {"char", JS_TYPE_CHAR}, + {"ipv4", JS_TYPE_IPV4}, + {"ipv6", JS_TYPE_IPV6}, + {"email", JS_TYPE_EMAIL}, + {"idn-email", JS_TYPE_IDN_EMAIL}, + {"hostname", JS_TYPE_HOSTNAME}, + {"idn-hostname", JS_TYPE_IDN_HOSTNAME}, + {"iri", JS_TYPE_IRI}, + {"iri-reference", JS_TYPE_IRI_REFERENCE}, + {"json-pointer", JS_TYPE_JSON_POINTER}, + {"relative-json-pointer", JS_TYPE_RELATIVE_JSON_POINTER}, +}; + +static const size_t g_type_names_count = sizeof(g_type_names) / sizeof(g_type_names[0]); + +const char* js_type_name(js_type_t type) { + for (size_t i = 0; i < g_type_names_count; ++i) { + if (g_type_names[i].type == type) { + return g_type_names[i].name; + } + } + return "unknown"; +} + +js_type_t js_type_from_name(const char* name) { + if (!name) return JS_TYPE_UNKNOWN; + + for (size_t i = 0; i < g_type_names_count; ++i) { + if (strcmp(name, g_type_names[i].name) == 0) { + return g_type_names[i].type; + } + } + return JS_TYPE_UNKNOWN; +} + +js_type_t js_type_of_json(const cJSON* json) { + if (!json) return JS_TYPE_UNKNOWN; + + if (cJSON_IsNull(json)) return JS_TYPE_NULL; + if (cJSON_IsBool(json)) return JS_TYPE_BOOLEAN; + if (cJSON_IsNumber(json)) return JS_TYPE_NUMBER; + if (cJSON_IsString(json)) return JS_TYPE_STRING; + if (cJSON_IsObject(json)) return JS_TYPE_OBJECT; + if (cJSON_IsArray(json)) return JS_TYPE_ARRAY; + + return JS_TYPE_UNKNOWN; +} + +/* ============================================================================ + * Error Functions + * ============================================================================ */ + +void js_error_init(js_error_t* error) { + if (error) { + error->code = 0; + error->severity = JS_SEVERITY_ERROR; + error->location.line = 0; + error->location.column = 0; + error->location.offset = 0; + error->path = NULL; + error->message = NULL; + } +} + +void js_error_cleanup(js_error_t* error) { + if (error) { + js_free(error->path); + js_free(error->message); + error->path = NULL; + error->message = NULL; + } +} + +bool js_error_set(js_error_t* error, js_error_code_t code, js_severity_t severity, + const char* path, const char* message) { + if (!error) return false; + + js_error_cleanup(error); + + error->code = code; + error->severity = severity; + error->path = path ? js_strdup(path) : NULL; + error->message = message ? js_strdup(message) : NULL; + + /* Check allocation success */ + if ((path && !error->path) || (message && !error->message)) { + js_error_cleanup(error); + return false; + } + + return true; +} + +/* ============================================================================ + * Result Functions + * ============================================================================ */ + +void js_result_init(js_result_t* result) { + if (result) { + result->valid = true; + result->errors = NULL; + result->error_count = 0; + result->error_capacity = 0; + } +} + +void js_result_cleanup(js_result_t* result) { + if (result && result->errors) { + for (size_t i = 0; i < result->error_count; ++i) { + js_error_cleanup(&result->errors[i]); + } + js_free(result->errors); + result->errors = NULL; + result->error_count = 0; + result->error_capacity = 0; + } +} + +static bool js_result_grow(js_result_t* result) { + if (result->error_count >= JS_MAX_ERRORS) { + return false; + } + + size_t new_capacity = result->error_capacity == 0 + ? JS_INITIAL_ERROR_CAPACITY + : result->error_capacity * 2; + + if (new_capacity > JS_MAX_ERRORS) { + new_capacity = JS_MAX_ERRORS; + } + + js_error_t* new_errors = (js_error_t*)js_realloc( + result->errors, new_capacity * sizeof(js_error_t)); + if (!new_errors) return false; + + result->errors = new_errors; + result->error_capacity = new_capacity; + return true; +} + +bool js_result_add_error(js_result_t* result, js_error_code_t code, + const char* message, const char* path) { + if (!result) return false; + + result->valid = false; + + if (result->error_count >= result->error_capacity) { + if (!js_result_grow(result)) return false; + } + + js_error_t* error = &result->errors[result->error_count]; + js_error_init(error); + error->code = code; + error->severity = JS_SEVERITY_ERROR; + error->path = path ? js_strdup(path) : NULL; + error->message = message ? js_strdup(message) : NULL; + + if ((path && !error->path) || (message && !error->message)) { + js_error_cleanup(error); + return false; + } + + result->error_count++; + return true; +} + +bool js_result_add_warning(js_result_t* result, js_error_code_t code, + const char* message, const char* path) { + if (!result) return false; + + if (result->error_count >= result->error_capacity) { + if (!js_result_grow(result)) return false; + } + + js_error_t* error = &result->errors[result->error_count]; + js_error_init(error); + error->code = code; + error->severity = JS_SEVERITY_WARNING; + error->path = path ? js_strdup(path) : NULL; + error->message = message ? js_strdup(message) : NULL; + + if ((path && !error->path) || (message && !error->message)) { + js_error_cleanup(error); + return false; + } + + result->error_count++; + return true; +} + +bool js_result_add_error_with_location(js_result_t* result, js_error_code_t code, + const char* message, const char* path, + js_location_t location) { + if (!result) return false; + + result->valid = false; + + if (result->error_count >= result->error_capacity) { + if (!js_result_grow(result)) return false; + } + + js_error_t* error = &result->errors[result->error_count]; + js_error_init(error); + error->code = code; + error->severity = JS_SEVERITY_ERROR; + error->location = location; + error->path = path ? js_strdup(path) : NULL; + error->message = message ? js_strdup(message) : NULL; + + if ((path && !error->path) || (message && !error->message)) { + js_error_cleanup(error); + return false; + } + + result->error_count++; + return true; +} + +bool js_result_merge(js_result_t* dest, const js_result_t* src) { + if (!dest || !src) return false; + + for (size_t i = 0; i < src->error_count; ++i) { + const js_error_t* e = &src->errors[i]; + if (e->severity == JS_SEVERITY_ERROR) { + if (!js_result_add_error_with_location(dest, e->code, e->message, + e->path, e->location)) { + return false; + } + } else { + if (!js_result_add_warning(dest, e->code, e->message, e->path)) { + return false; + } + } + } + + return true; +} + +char* js_result_to_string(const js_result_t* result) { + if (!result) return NULL; + + /* Calculate required buffer size with overflow checking */ + size_t total_len = 0; + for (size_t i = 0; i < result->error_count; ++i) { + const js_error_t* e = &result->errors[i]; + /* Format: "[SEVERITY] path: message\n" */ + size_t entry_len = 20; /* Severity + brackets + colon + spaces + newline */ + if (e->path) entry_len += strlen(e->path); + if (e->message) entry_len += strlen(e->message); + + /* Check for overflow */ + if (total_len > SIZE_MAX - entry_len) { + return NULL; /* Would overflow */ + } + total_len += entry_len; + } + + if (total_len == 0) { + return js_strdup(result->valid ? "Valid" : "Invalid (no errors recorded)"); + } + + /* Check for final overflow when adding null terminator */ + if (total_len > SIZE_MAX - 1) { + return NULL; + } + + char* buffer = (char*)js_malloc(total_len + 1); + if (!buffer) return NULL; + + char* ptr = buffer; + size_t remaining = total_len + 1; + for (size_t i = 0; i < result->error_count; ++i) { + const js_error_t* e = &result->errors[i]; + const char* sev = e->severity == JS_SEVERITY_ERROR ? "ERROR" + : e->severity == JS_SEVERITY_WARNING ? "WARNING" + : "INFO"; + int written = snprintf(ptr, remaining, "[%s] %s: %s\n", + sev, + e->path ? e->path : "", + e->message ? e->message : ""); + if (written < 0 || (size_t)written >= remaining) { + break; /* Truncated or error - stop */ + } + ptr += written; + remaining -= (size_t)written; + } + + return buffer; +} + +/* ============================================================================ + * Location Functions + * ============================================================================ */ + +js_location_t js_location_make(int line, int column, size_t offset) { + js_location_t loc = { line, column, offset }; + return loc; +} + +bool js_location_is_valid(js_location_t location) { + return location.line > 0; +} diff --git a/r/tests/testthat/helper-jsonstructure.R b/r/tests/testthat/helper-jsonstructure.R index 61cafb5..171ee78 100644 --- a/r/tests/testthat/helper-jsonstructure.R +++ b/r/tests/testthat/helper-jsonstructure.R @@ -1,36 +1,15 @@ # Shared test helpers. -# Skip a test unless the prebuilt json_structure library is *already* available -# locally (via JSONSTRUCTURE_LIB_PATH or a previously installed cache copy). -# This never triggers a download, so the suite stays fully offline/network-free -# and CRAN-safe: it runs where JSONSTRUCTURE_LIB_PATH points at a fresh build -# (e.g. CI) and skips cleanly everywhere else. +# The JSON Structure C engine is compiled directly into the package, so +# validation is always available: there is no optional binary to load and +# nothing to skip. This shim is retained as a no-op so the existing test files +# read unchanged; the tests now run in every environment, including on CRAN. skip_if_no_binding <- function() { - testthat::skip_on_cran() - - if (isTRUE(tryCatch(jsonstructure:::js_binding_loaded(), - error = function(e) FALSE))) { - return(invisible()) - } - - path <- tryCatch(jsonstructure::jsonstructure_binary_path(), - error = function(e) NULL) - if (is.null(path)) { - testthat::skip("json_structure prebuilt library not available") - } - - ok <- tryCatch({ - jsonstructure:::.js_load_binding(path) - jsonstructure:::js_binding_loaded() - }, error = function(e) FALSE) - if (!isTRUE(ok)) { - testthat::skip("json_structure prebuilt library could not be loaded") - } invisible() } # Construct a base (C-side) result to feed the pure-R augmentation directly, -# without requiring the binary. +# without invoking the engine. valid_base_result <- function() { jsonstructure:::new_validation_result(TRUE, list()) } diff --git a/r/tools/vendor-engine.R b/r/tools/vendor-engine.R new file mode 100644 index 0000000..f937e9a --- /dev/null +++ b/r/tools/vendor-engine.R @@ -0,0 +1,116 @@ +#!/usr/bin/env Rscript +# --------------------------------------------------------------------------- +# vendor-engine.R +# +# Refreshes the copy of the JSON Structure C engine that is vendored into this +# R package (r/src/) from the upstream C SDK sources (c/). The R package +# compiles the engine from these bundled sources at install time, so this +# script is the single point that keeps them in sync with c/. +# +# Run from anywhere inside the repository: +# +# Rscript r/tools/vendor-engine.R +# +# WHAT IS SYNCED (copied verbatim from c/ -> r/src/): +# * engine C sources c/src/*.c -> r/src/ +# * regex header c/src/regex_utils.h -> r/src/ +# * public engine headers c/include/json_structure/*.h -> r/src/json_structure/ +# +# WHAT IS NOT SYNCED (R-specific, hand-maintained -- never overwritten here): +# * r/src/shim.c R .Call marshaling for the engine ABI +# * r/src/init.c R native-routine registration / engine init +# * r/src/regex_utils.c pure-C ECMAScript-subset matcher used INSTEAD of the +# upstream c/src/regex_utils.cpp (std::regex), because +# MinGW std::regex deadlocks inside an R-loaded DLL on +# Windows. Keep this file; do not replace it with the +# C++ upstream. +# * r/src/cJSON.c, r/src/cjson/cJSON.h +# cJSON is pinned at v1.7.18 (MIT, (c) Dave Gamble) and +# vendored directly from the cJSON project, not from c/. +# Update it deliberately, not through this script. +# --------------------------------------------------------------------------- + +find_repo_root <- function() { + # Walk up from this script's location until we find both c/ and r/. + args <- commandArgs(trailingOnly = FALSE) + file_arg <- sub("^--file=", "", args[grepl("^--file=", args)]) + start <- if (length(file_arg) == 1L && nzchar(file_arg)) { + dirname(normalizePath(file_arg)) + } else { + getwd() + } + dir <- start + for (i in seq_len(8)) { + if (dir.exists(file.path(dir, "c", "src")) && + dir.exists(file.path(dir, "r", "src"))) { + return(dir) + } + parent <- dirname(dir) + if (identical(parent, dir)) break + dir <- parent + } + stop("could not locate the repository root (expected sibling c/ and r/ dirs)") +} + +# Engine C translation units shared with the C SDK. +ENGINE_SOURCES <- c( + "error_codes.c", + "instance_validator.c", + "json_source_locator.c", + "schema_validator.c", + "types.c" +) + +# The regex ABI header is shared; the implementation (regex_utils.c) is not. +SHARED_SRC_HEADERS <- c( + "regex_utils.h" +) + +# Public engine headers (c/include/json_structure/ -> r/src/json_structure/). +ENGINE_HEADERS <- c( + "error_codes.h", + "export.h", + "instance_validator.h", + "json.h", + "json_structure.h", + "schema_validator.h", + "types.h" +) + +copy_one <- function(from, to) { + if (!file.exists(from)) { + stop(sprintf("missing upstream source: %s", from)) + } + ok <- file.copy(from, to, overwrite = TRUE, copy.date = TRUE) + if (!ok) stop(sprintf("failed to copy %s -> %s", from, to)) + invisible(TRUE) +} + +main <- function() { + root <- find_repo_root() + c_src <- file.path(root, "c", "src") + c_inc <- file.path(root, "c", "include", "json_structure") + r_src <- file.path(root, "r", "src") + r_inc <- file.path(r_src, "json_structure") + + dir.create(r_inc, showWarnings = FALSE, recursive = TRUE) + + n <- 0L + for (f in c(ENGINE_SOURCES, SHARED_SRC_HEADERS)) { + copy_one(file.path(c_src, f), file.path(r_src, f)) + message(sprintf(" synced c/src/%-24s -> r/src/%s", f, f)) + n <- n + 1L + } + for (h in ENGINE_HEADERS) { + copy_one(file.path(c_inc, h), file.path(r_inc, h)) + message(sprintf(" synced c/include/json_structure/%-16s -> r/src/json_structure/%s", h, h)) + n <- n + 1L + } + + message(sprintf("\nDone: %d file(s) synced from c/ into r/src/.", n)) + message("Left untouched (R-specific): shim.c, init.c, regex_utils.c, cJSON.c, cjson/cJSON.h") + message("Reminder: r/src/regex_utils.c replaces the upstream C++ std::regex") + message("implementation and must be kept in place for the Windows build.") +} + +main() From 9c7c2be37c87bfd3cd520fbdbe9f410233d20713 Mon Sep 17 00:00:00 2001 From: Clemens Vasters Date: Tue, 14 Jul 2026 09:55:38 +0200 Subject: [PATCH 2/2] Use snprintf in vendored cJSON for CRAN compiled-code check The CRAN "checking compiled code" check flags `__sprintf_chk` (from `sprintf`) on Linux/macOS as a disallowed entry point, producing a WARNING that fails `R CMD check --as-cran` under error-on=warning. (MinGW on Windows does not emit the fortified symbol, so the local Windows check did not surface it.) Replace all `sprintf()` calls in the vendored `src/cJSON.c` with bounded `snprintf()` (using the target buffer sizes). Behavior is unchanged since the buffers were already adequately sized; the package's testthat corpus still passes. Disclose the modification in cran-comments.md, inst/COPYRIGHTS, the cJSON.c header, and NEWS.md, and correct the COPYRIGHTS engine file list to reference regex_utils.c (pure C) rather than the upstream regex_utils.cpp. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- r/NEWS.md | 2 ++ r/cran-comments.md | 10 ++++++---- r/inst/COPYRIGHTS | 8 +++++++- r/src/cJSON.c | 16 ++++++++++------ 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/r/NEWS.md b/r/NEWS.md index b39cfdd..dc8d9ff 100644 --- a/r/NEWS.md +++ b/r/NEWS.md @@ -36,3 +36,5 @@ shared library downloaded from GitHub Releases on first use. platform with no C++/`std::regex` dependency. Constructs outside the subset (lookbehind, named groups, inline flags, Unicode property escapes) are treated as invalid patterns, and backtracking is bounded against pathological inputs. +* The vendored `cJSON` sources use bounded `snprintf()` (in place of the + upstream `sprintf()`) so the package passes the CRAN "compiled code" check. diff --git a/r/cran-comments.md b/r/cran-comments.md index 3e0dfed..d9289bd 100644 --- a/r/cran-comments.md +++ b/r/cran-comments.md @@ -7,13 +7,15 @@ * checking pragmas in C/C++ headers and code ... NOTE File which contains pragma(s) suppressing diagnostics: 'src/cJSON.c' - `src/cJSON.c` is the unmodified, vendored cJSON library (v1.7.18, MIT, + `src/cJSON.c` is the vendored cJSON library (v1.7.18, MIT, © Dave Gamble and cJSON contributors). It contains a `#pragma GCC diagnostic ignored "-Wcast-qual"` in its upstream source. The - file is bundled verbatim so that the package's native engine has no external + file is bundled so that the package's native engine has no external dependency; the pragma only takes effect under `-Wcast-qual`, which is not part - of the default R compilation flags. Authorship and copyright are recorded in - `DESCRIPTION` and `inst/COPYRIGHTS`. + of the default R compilation flags. The only modification to the upstream + source is replacing its `sprintf()` calls with bounded `snprintf()` to satisfy + the CRAN "compiled code" check (no `sprintf`/`__sprintf_chk` entry point). + Authorship and copyright are recorded in `DESCRIPTION` and `inst/COPYRIGHTS`. ## Test environments diff --git a/r/inst/COPYRIGHTS b/r/inst/COPYRIGHTS index 8a719c5..92b1718 100644 --- a/r/inst/COPYRIGHTS +++ b/r/inst/COPYRIGHTS @@ -6,11 +6,14 @@ License, compatible with this package's license. JSON Structure C engine Files: src/types.c, src/error_codes.c, src/schema_validator.c, src/instance_validator.c, src/json_source_locator.c, - src/regex_utils.cpp, src/regex_utils.h, + src/regex_utils.c, src/regex_utils.h, src/json_structure/*.h Copyright: (c) 2024 JSON Structure Contributors License: MIT (SPDX-License-Identifier: MIT) Source: https://github.com/json-structure/sdk (directory: c/) + Note: src/regex_utils.c is an R-package-specific pure-C ECMAScript-subset + matcher used in place of the upstream C++ (std::regex) + implementation, which deadlocks in an R-loaded DLL on Windows. ------------------------------------------------------------------------------- cJSON @@ -19,4 +22,7 @@ cJSON Copyright: (c) 2009-2017 Dave Gamble and cJSON contributors License: MIT (SPDX-License-Identifier: MIT) Source: https://github.com/DaveGamble/cJSON (tag v1.7.18) + Note: src/cJSON.c is vendored from the tag above with one change: its + sprintf() calls are replaced by bounded snprintf() so the package + passes the CRAN "compiled code" entry-point check. ------------------------------------------------------------------------------- diff --git a/r/src/cJSON.c b/r/src/cJSON.c index 61483d9..a5dbec4 100644 --- a/r/src/cJSON.c +++ b/r/src/cJSON.c @@ -1,6 +1,10 @@ /* Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + Vendored for the jsonstructure R package from cJSON v1.7.18. The only local + change versus upstream is that sprintf() calls are replaced by bounded + snprintf() so the package passes the CRAN "compiled code" entry-point check. + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights @@ -124,7 +128,7 @@ CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item) CJSON_PUBLIC(const char*) cJSON_Version(void) { static char version[15]; - sprintf(version, "%i.%i.%i", CJSON_VERSION_MAJOR, CJSON_VERSION_MINOR, CJSON_VERSION_PATCH); + snprintf(version, sizeof(version), "%i.%i.%i", CJSON_VERSION_MAJOR, CJSON_VERSION_MINOR, CJSON_VERSION_PATCH); return version; } @@ -568,22 +572,22 @@ static cJSON_bool print_number(const cJSON * const item, printbuffer * const out /* This checks for NaN and Infinity */ if (isnan(d) || isinf(d)) { - length = sprintf((char*)number_buffer, "null"); + length = snprintf((char*)number_buffer, sizeof(number_buffer), "null"); } else if(d == (double)item->valueint) { - length = sprintf((char*)number_buffer, "%d", item->valueint); + length = snprintf((char*)number_buffer, sizeof(number_buffer), "%d", item->valueint); } else { /* Try 15 decimal places of precision to avoid nonsignificant nonzero digits */ - length = sprintf((char*)number_buffer, "%1.15g", d); + length = snprintf((char*)number_buffer, sizeof(number_buffer), "%1.15g", d); /* Check whether the original double can be recovered */ if ((sscanf((char*)number_buffer, "%lg", &test) != 1) || !compare_double((double)test, d)) { /* If not, print with 17 decimal places of precision */ - length = sprintf((char*)number_buffer, "%1.17g", d); + length = snprintf((char*)number_buffer, sizeof(number_buffer), "%1.17g", d); } } @@ -1017,7 +1021,7 @@ static cJSON_bool print_string_ptr(const unsigned char * const input, printbuffe break; default: /* escape and print as unicode codepoint */ - sprintf((char*)output_pointer, "u%04x", *input_pointer); + snprintf((char*)output_pointer, 6, "u%04x", *input_pointer); output_pointer += 4; break; }