diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14adffc..2537181 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,7 +85,7 @@ jobs: - name: Run busted tests (under LuaJIT) run: | - # ffi.load("quickdecode") uses dlopen which respects LD_LIBRARY_PATH, + # ffi.load("qjson") uses dlopen which respects LD_LIBRARY_PATH, # not LuaJIT's package.cpath. Point dlopen at the release build dir. LD_LIBRARY_PATH="$PWD/target/release" \ busted --lua=$(which luajit) tests/lua \ diff --git a/CLAUDE.md b/CLAUDE.md index f2ab0c8..01885c7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,16 +4,16 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project -Rust JSON decoder (`cdylib` + `rlib`) exposed to LuaJIT via FFI. Optimized for parse-once / extract-a-few-fields / discard. The competitive edge over `lua-cjson` comes from **never building a Lua table** — Phase 1 records only structural offsets, Phase 2 lazily decodes the fields the caller actually asks for. Crate name in `Cargo.toml` is `lua-quick-decode`; the compiled artifact is `libquickdecode.so`. +Rust JSON decoder (`cdylib` + `rlib`) exposed to LuaJIT via FFI. Optimized for parse-once / extract-a-few-fields / discard. The competitive edge over `lua-cjson` comes from **never building a Lua table** — Phase 1 records only structural offsets, Phase 2 lazily decodes the fields the caller actually asks for. Crate name in `Cargo.toml` is `qjson`; the compiled artifact is `libqjson.so`. ## Common commands The `Makefile` is the canonical entry point; `make help` lists targets. ```sh -make build # cargo build --release → target/release/libquickdecode.so +make build # cargo build --release → target/release/libqjson.so make test # cargo test --release + busted Lua tests -make lint # cargo clippy -D warnings + cargo fmt --check +make lint # cargo clippy --release --all-targets -- -D warnings make bench # OpenResty LuaJIT benchmark vs lua-cjson and simdjson ``` @@ -37,7 +37,7 @@ cargo test --release --no-default-features cargo test --features test-panic --release ``` -`ffi.load("quickdecode")` uses `dlopen`, which respects `LD_LIBRARY_PATH` — **not** LuaJIT's `package.cpath`. The Makefile sets `LD_LIBRARY_PATH=target/release` for `test`/`bench`; if you invoke `busted` or `luajit` directly, set it yourself. +`ffi.load("qjson")` uses `dlopen`, which respects `LD_LIBRARY_PATH` — **not** LuaJIT's `package.cpath`. The Makefile sets `LD_LIBRARY_PATH=target/release` for `test`/`bench`; if you invoke `busted` or `luajit` directly, set it yourself. `make lint` runs clippy only (with `-D warnings`); `cargo fmt --check` is intentionally **not** part of the lint gate because the codebase uses manual column alignment in struct definitions and compact single-line literals that default rustfmt would reflow. See the README "Roadmap / Deferred" entry on fmt for context. @@ -50,39 +50,39 @@ cargo test --features test-panic --release - `Avx2Scanner` (gated by the `avx2` cargo feature, default-on) when both `avx2` and `pclmulqdq` are detected at runtime. - `ScalarScanner` otherwise. -Validation level depends on `qjd_options.mode`. **EAGER** (default): a post-scan pass walks `indices` and validates RFC 8259 number ABNF, string content (no unescaped control chars), and UTF-8 — parse fails on any value-level violation. **LAZY** (opt-in): bracket/quote balance + max-depth only; value-level errors surface when the offending field is accessed (lua-cjson-equivalent behavior). Trailing-content rejection and value-level validation are eager-only; max-depth (default 1024, configurable up to 4096) is enforced in both modes. +Validation level depends on `qjson_options.mode`. **EAGER** (default): a post-scan pass walks `indices` and validates RFC 8259 number ABNF, string content (no unescaped control chars), and UTF-8 — parse fails on any value-level violation. **LAZY** (opt-in): bracket/quote balance + max-depth only; value-level errors surface when the offending field is accessed (lua-cjson-equivalent behavior). Trailing-content rejection and value-level validation are eager-only; max-depth (default 1024, configurable up to 4096) is enforced in both modes. **Phase 2** (`src/cursor.rs`, `src/path.rs`, `src/decode/`): path strings are parsed by a zero-alloc `PathIter` into `PathSeg::Key | Idx`. A `Cursor` (a `(idx_start, idx_end)` pair into `doc.indices`) is walked to the target, optionally caching sibling spans in `doc.skip` (`SkipCache`) so repeated lookups on the same container skip brace-counting. Strings are decoded into `doc.scratch` only when they contain escapes; otherwise the original buffer slice is handed back. ### Critical invariants (these will bite you if violated) -- **`get_str` pointer lifetime.** The `(ptr, len)` returned by `qjd_get_str` / `qjd_cursor_get_str` points into either the original input buffer or `doc.scratch`. **Any subsequent `*_get_str` call on the same doc may invalidate prior pointers** (scratch buffer reuse). The LuaJIT wrapper preserves this contract by calling `ffi.string(ptr, len)` immediately to copy into a Lua string — do not change that. -- **Buffer lifetime.** `Document<'a>` borrows the input slice. `qjd_parse` transmutes `'a` to `'static` and trusts the caller to keep the buffer alive for the document's lifetime. The LuaJIT wrapper enforces this by stashing the original string under `_hold` on the Doc table so Lua GC keeps it pinned. +- **`get_str` pointer lifetime.** The `(ptr, len)` returned by `qjson_get_str` / `qjson_cursor_get_str` points into either the original input buffer or `doc.scratch`. **Any subsequent `*_get_str` call on the same doc may invalidate prior pointers** (scratch buffer reuse). The LuaJIT wrapper preserves this contract by calling `ffi.string(ptr, len)` immediately to copy into a Lua string — do not change that. +- **Buffer lifetime.** `Document<'a>` borrows the input slice. `qjson_parse` transmutes `'a` to `'static` and trusts the caller to keep the buffer alive for the document's lifetime. The LuaJIT wrapper enforces this by stashing the original string under `_hold` on the Doc table so Lua GC keeps it pinned. - **`indices` stores offsets only, not types.** Token type is recovered from `buf[indices[i]]`. Do not add a type tag — the 25% memory win is intentional. -- **Single-threaded.** `qjd_doc` is not Sync/Send across threads; `RefCell` is used for `scratch` and `skip`. -- **FFI panic barrier.** Every `pub unsafe extern "C"` function in `src/ffi.rs` wraps its body in `catch_unwind` and converts a panic into `QJD_OOM`. Preserve this pattern on any new export — a panic crossing the FFI boundary is undefined behavior. +- **Single-threaded.** `qjson_doc` is not Sync/Send across threads; `RefCell` is used for `scratch` and `skip`. +- **FFI panic barrier.** Every `pub unsafe extern "C"` function in `src/ffi.rs` wraps its body in `catch_unwind` and converts a panic into `QJSON_OOM`. Preserve this pattern on any new export — a panic crossing the FFI boundary is undefined behavior. ### Layout ``` src/ lib.rs crate root - ffi.rs extern "C" surface, qjd_* symbols, panic barrier + ffi.rs extern "C" surface, qjson_* symbols, panic barrier doc.rs Document (indices + scratch + skip cache) cursor.rs Cursor + path resolution + skip-cache walk path.rs zero-alloc path-string iterator decode/ lazy string / number decode scan/ ScalarScanner, Avx2Scanner, runtime dispatch skip_cache.rs Phase 2 sibling-skip cache - error.rs qjd_err + qjd_type enums (must stay in sync with include/lua_quick_decode.h and lua/quickdecode.lua) + error.rs qjson_err + qjson_type enums (must stay in sync with include/qjson.h and lua/qjson.lua) -lua/quickdecode.lua LuaJIT wrapper (ffi.cdef + Doc/Cursor metatables) -include/lua_quick_decode.h public C header +lua/qjson.lua LuaJIT wrapper (ffi.cdef + Doc/Cursor metatables) +include/qjson.h public C header tests/ Rust integration tests + tests/lua/ busted suite benches/ lua_bench.lua vs lua-cjson/simdjson; fixtures/ has small_api.json + medium_resp.json ``` -The enum values in `src/error.rs` are duplicated in `include/lua_quick_decode.h` and `lua/quickdecode.lua` (the latter only encodes the `T_*` type tags and `NOT_FOUND = 2`). Keep all three in sync when adding/renumbering codes. +The enum values in `src/error.rs` are duplicated in `include/qjson.h` and `lua/qjson.lua` (the latter only encodes the `T_*` type tags and `NOT_FOUND = 2`). Keep all three in sync when adding/renumbering codes. ### CI gates worth knowing diff --git a/Cargo.toml b/Cargo.toml index 98433a9..7089b77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,14 @@ [package] -name = "lua-quick-decode" +name = "qjson" version = "0.1.0" edition = "2021" -publish = false +description = "Fast JSON decoder for LuaJIT FFI consumers" +license = "Apache-2.0" +repository = "https://github.com/api7/lua-qjson" +homepage = "https://github.com/api7/lua-qjson" [lib] -name = "quickdecode" +name = "qjson" crate-type = ["cdylib", "rlib"] [features] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d9a10c0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/Makefile b/Makefile index 086b577..628ee82 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ help: ## Show this help @# Consequence: targets whose prerequisite list contains `#` won't render — none today. @awk 'BEGIN {FS = ":[^#]*## "} /^[a-zA-Z_-]+:[^#]*## / {printf " \033[36m%-10s\033[0m — %s\n", $$1, $$2}' $(MAKEFILE_LIST) -build: ## Build the release cdylib (target/release/libquickdecode.so) +build: ## Build the release cdylib (target/release/libqjson.so) cargo build --release test: build ## Run cargo tests + busted Lua tests diff --git a/README.md b/README.md index 18d93c8..64f34d7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# lua-quick-decode +# qjson Rust-implemented fast JSON decoder exposed to LuaJIT via FFI. Optimized for the common case where a large JSON is parsed once and only a small number of fields are extracted before the document is discarded. @@ -10,7 +10,7 @@ Initial implementation complete: scalar + AVX2/PCLMUL + ARM64 NEON/PMULL structu ```sh cargo build --release -# Output: target/release/libquickdecode.so +# Output: target/release/libqjson.so ``` A `Makefile` wraps the common workflows; run `make help` to see `build`, `test`, `lint`, `bench`, and `clean` targets. Override `LUAJIT` / `LUA_CPATH` per invocation if your environment differs from the defaults. @@ -18,14 +18,15 @@ A `Makefile` wraps the common workflows; run `make help` to see `build`, `test`, ## Testing ```sh -cargo test +git submodule update --init --recursive +cargo test --release ``` ## LuaJIT Usage ```lua -local qd = require("quickdecode") -local doc = qd.parse(json_str) +local qjson = require("qjson") +local doc = qjson.parse(json_str) -- Root-path getter: local model = doc:get_str("body.model") @@ -36,39 +37,39 @@ local model = body:get_str("model") local temp = body:get_f64("temperature") ``` -### Lazy table API (`qd.decode` / `qd.encode`) +### Lazy table API (`qjson.decode` / `qjson.encode`) For callers migrating from `cjson`, an alternative API returns a table-shaped lazy view. Reads, iteration, and length all work like a `cjson.decode`'d table; writes materialize the affected level into a plain Lua table. ```lua -local qd = require("quickdecode") +local qjson = require("qjson") local cjson = require("cjson") -- optional; provides null / empty_array sentinels -local t = qd.decode(json_str) +local t = qjson.decode(json_str) print(t.model) -for _, m in qd.ipairs(t.messages) do +for _, m in qjson.ipairs(t.messages) do print(m.role, m.content) end t.extra = "x" -local s = qd.encode(t) -- drop-in replacement for cjson.encode +local s = qjson.encode(t) -- drop-in replacement for cjson.encode ``` -`qd.encode` works on lazy proxies (re-emitting unmodified subtrees as the +`qjson.encode` works on lazy proxies (re-emitting unmodified subtrees as the original JSON bytes), real Lua tables (matching `cjson.encode` output), and mixed trees. Callers cannot pass a lazy proxy directly to `cjson.encode` -(cjson bypasses metamethods in C); use `qd.encode` instead, or call -`qd.materialize(t)` to get a plain Lua table that any third-party encoder +(cjson bypasses metamethods in C); use `qjson.encode` instead, or call +`qjson.materialize(t)` to get a plain Lua table that any third-party encoder can handle. **LuaJIT compat-52 caveat.** `for k, v in pairs/ipairs(t)` and `#t` on a lazy proxy rely on `__pairs` / `__ipairs` / `__len`, which LuaJIT only invokes when built with `LUAJIT_ENABLE_LUA52COMPAT` (OpenResty's default). On a stock LuaJIT -5.1, use the explicit `qd.pairs(t)`, `qd.ipairs(t)`, and `qd.len(t)` helpers +5.1, use the explicit `qjson.pairs(t)`, `qjson.ipairs(t)`, and `qjson.len(t)` helpers — they work on both builds. ## Testing — Lua @@ -77,26 +78,27 @@ Requires LuaJIT + busted + lua-cjson installed system-wide. ```sh cargo build --release -busted tests/lua --lpath='./lua/?.lua' --cpath='./target/release/lib?.so' +LD_LIBRARY_PATH="$PWD/target/release" \ + busted --lua="$(which luajit)" tests/lua --lpath='./lua/?.lua' ``` ## Benchmarks -`quickdecode` vs. `lua-cjson` and `lua-resty-simdjson` on multimodal +`qjson` vs. `lua-cjson` and `lua-resty-simdjson` on multimodal chat-completion payloads, "parse + access model, temperature, and all messages[*].content paths" workload (median ops/s under OpenResty LuaJIT 2.1, Intel Core i5-9400; 5 rounds, deterministic payload): -| Size | cjson | simdjson | `qd.parse` | `qd.decode + access content` | speedup vs. cjson | +| Size | cjson | simdjson | `qjson.parse` | `qjson.decode + access content` | speedup vs. cjson | |---:|---:|---:|---:|---:|---:| | 2 KB | 106,646 | 137,427 | 135,296 | 97,574 | 1.3× / 0.9× | | 100 KB | 6,045 | 46,577 | 137,931 | 134,590 | 22.8× / 22.3× | | 1 MB | 594 | 4,408 | 16,447 | 16,340 | 27.7× / 27.5× | | 10 MB | 59 | 356 | 1,035 | 1,028 | 17.5× / 17.4× | -`qd.parse` wins because it skips building a Lua table for the parts you -never read; `qd.decode + t.field` adds a cjson-shaped table proxy on top -with similar throughput. Memory retention for `quickdecode` is essentially +`qjson.parse` wins because it skips building a Lua table for the parts you +never read; `qjson.decode + t.field` adds a cjson-shaped table proxy on top +with similar throughput. Memory retention for `qjson` is essentially flat in payload size (a few KB for the reusable buffers), while `cjson` and `simdjson` retain more Lua heap because they materialize the table tree. @@ -107,7 +109,7 @@ uses `lua-resty-simdjson` when `resty.simdjson` is available in the OpenResty environment; otherwise it skips the simdjson rows. ```sh -make bench # quickdecode vs cjson and lua-resty-simdjson +make bench # qjson vs cjson and lua-resty-simdjson ``` ## RFC 8259 conformance @@ -127,19 +129,22 @@ reject malformed payloads before forwarding them upstream. From Lua: ```lua -local doc = qd.parse(json) -- eager (default) -local doc = qd.parse(json, { lazy = true }) -- lazy mode -local doc = qd.parse(json, { max_depth = 256 }) -- stricter depth limit -local doc = qd.parse(json, { lazy = true, max_depth = 256 }) +local doc = qjson.parse(json) -- eager (default) +local doc = qjson.parse(json, { lazy = true }) -- lazy mode +local doc = qjson.parse(json, { max_depth = 256 }) -- stricter depth limit +local doc = qjson.parse(json, { lazy = true, max_depth = 256 }) ``` From C: ```c -qjd_options opts = { .mode = QJD_MODE_LAZY, .max_depth = 256 }; -qjd_doc* doc = qjd_parse_ex(buf, len, &opts, &err); +qjson_options opts = { .mode = QJSON_MODE_LAZY, .max_depth = 256 }; +qjson_doc* doc = qjson_parse_ex(buf, len, &opts, &err); ``` ### Known gaps -Three structural-grammar checks are deferred to a follow-up — they require a grammar-aware walk beyond the current heuristic. See `tests/rfc8259_compliance.rs` for the specific `#[ignore]`d cases, and `tests/json_test_suite.rs::KNOWN_N_FAILURES` for the corresponding JSONTestSuite files. +There are no known strict-mode structural grammar gaps at this time: +`tests/json_test_suite.rs::KNOWN_N_FAILURES` is empty, and the RFC 8259 +suite has no ignored structural cases. Update this section whenever a +temporary conformance exception is introduced. diff --git a/benches/lua_bench.lua b/benches/lua_bench.lua index a1b26d4..30a3977 100644 --- a/benches/lua_bench.lua +++ b/benches/lua_bench.lua @@ -1,7 +1,7 @@ package.path = package.path .. ";./lua/?.lua" package.cpath = package.cpath .. ";./target/release/lib?.so" -local qd = require("quickdecode") +local qjson = require("qjson") local cjson = require("cjson") local simdjson_ok, simdjson_or_err = pcall(function() return require("resty.simdjson").new() @@ -199,7 +199,7 @@ local function content_paths(n) return paths end -local function default_qd_access(d) +local function default_qjson_access(d) local _ = d:get_str("model") local _ = d:get_f64("temperature") local n = d:len("messages") or 0 @@ -213,7 +213,7 @@ local function default_table_access(t) local _ = t.model local _ = t.temperature if t.messages then - for i = 1, qd.len(t.messages) do + for i = 1, qjson.len(t.messages) do local msg = t.messages[i] local _ = msg.content end @@ -227,7 +227,7 @@ local function github_cjson_access(obj) local _ = obj[1] and obj[1].user and obj[1].user.login end -local function github_qd_access(d) +local function github_qjson_access(d) local _ = d:get_i64("[0].id") local _ = d:get_str("[0].title") local _ = d:get_str("[0].user.login") @@ -243,7 +243,7 @@ local scenarios = { {name = "small", iters = 5000, payload = read_file("benches/fixtures/small_api.json")}, {name = "medium", iters = 500, payload = read_file("benches/fixtures/medium_resp.json")}, {name = "github-100k", iters = 100, payload = make_github_issues_payload(100 * 1024), - cjson_access = github_cjson_access, qd_access = github_qd_access, table_access = github_table_access}, + cjson_access = github_cjson_access, qjson_access = github_qjson_access, table_access = github_table_access}, {name = "100k", iters = 100, payload = make_payload(100 * 1024)}, {name = "200k", iters = 50, payload = make_payload(200 * 1024)}, {name = "500k", iters = 20, payload = make_payload(500 * 1024)}, @@ -253,10 +253,10 @@ local scenarios = { {name = "10m", iters = 20, payload = make_payload(10 * 1024 * 1024)}, } --- The pooled API (qd.new_decoder + :parse) only exists on commits that +-- The pooled API (qjson.new_decoder + :parse) only exists on commits that -- landed the Decoder refactor. Probe so the bench still runs on older builds. -local has_pooled_api = type(qd.new_decoder) == "function" -local pooled_decoder = has_pooled_api and qd.new_decoder() or nil +local has_pooled_api = type(qjson.new_decoder) == "function" +local pooled_decoder = has_pooled_api and qjson.new_decoder() or nil if not simdjson then print("lua-resty-simdjson unavailable; skipping simdjson rows: " @@ -267,7 +267,7 @@ for _, s in ipairs(scenarios) do print(string.format("=== %s (%d bytes) ===", s.name, #s.payload)) local cjson_access = s.cjson_access or default_cjson_access - local qd_access = s.qd_access or default_qd_access + local qjson_access = s.qjson_access or default_qjson_access local table_access = s.table_access or default_table_access bench("cjson.decode + access fields", s.iters, function() @@ -282,32 +282,32 @@ for _, s in ipairs(scenarios) do end) end - bench("quickdecode.parse + access fields", s.iters, function() - local d = qd.parse(s.payload) - qd_access(d) + bench("qjson.parse + access fields", s.iters, function() + local d = qjson.parse(s.payload) + qjson_access(d) end) if has_pooled_api then - bench("quickdecode pooled :parse + access fields", s.iters, function() + bench("qjson pooled :parse + access fields", s.iters, function() local d = pooled_decoder:parse(s.payload) - qd_access(d) + qjson_access(d) end) - bench("quickdecode new_decoder()+parse (one-shot)", s.iters, function() - local dec = qd.new_decoder() + bench("qjson new_decoder()+parse (one-shot)", s.iters, function() + local dec = qjson.new_decoder() local d = dec:parse(s.payload) - qd_access(d) + qjson_access(d) end) end - bench("qd.decode + access content", s.iters, function() - local t = qd.decode(s.payload) + bench("qjson.decode + access content", s.iters, function() + local t = qjson.decode(s.payload) table_access(t) end) - bench("qd.decode + qd.encode (unmodified)", s.iters, function() - local t = qd.decode(s.payload) - local _ = qd.encode(t) + bench("qjson.decode + qjson.encode (unmodified)", s.iters, function() + local t = qjson.decode(s.payload) + local _ = qjson.encode(t) end) end @@ -358,32 +358,32 @@ do end next_p = make_cycler(interleaved) - bench("quickdecode.parse + access fields", 400, function() + bench("qjson.parse + access fields", 400, function() local p = next_p() - local d = qd.parse(p) - default_qd_access(d) + local d = qjson.parse(p) + default_qjson_access(d) end) if has_pooled_api then next_p = make_cycler(interleaved) - bench("quickdecode pooled :parse + access fields", 400, function() + bench("qjson pooled :parse + access fields", 400, function() local p = next_p() local d = pooled_decoder:parse(p) - default_qd_access(d) + default_qjson_access(d) end) end next_p = make_cycler(interleaved) - bench("qd.decode + access content", 400, function() + bench("qjson.decode + access content", 400, function() local p = next_p() - local t = qd.decode(p) + local t = qjson.decode(p) default_table_access(t) end) next_p = make_cycler(interleaved) - bench("qd.decode + qd.encode (unmodified)", 400, function() + bench("qjson.decode + qjson.encode (unmodified)", 400, function() local p = next_p() - local t = qd.decode(p) - local _ = qd.encode(t) + local t = qjson.decode(p) + local _ = qjson.encode(t) end) end diff --git a/benches/perf_probe.lua b/benches/perf_probe.lua index 4b0231e..5228704 100644 --- a/benches/perf_probe.lua +++ b/benches/perf_probe.lua @@ -1,11 +1,11 @@ --- Minimal probe for perf: hammers qd.parse on a fixed 100K payload so perf +-- Minimal probe for perf: hammers qjson.parse on a fixed 100K payload so perf -- samples concentrate on the FFI entry + parse hot path. Not a benchmark — -- there is no timing or memory accounting here, just sustained work. package.path = package.path .. ";./lua/?.lua" package.cpath = package.cpath .. ";./target/release/lib?.so" -local qd = require("quickdecode") +local qjson = require("qjson") -- Same payload generator as lua_bench.lua so probe output corresponds to -- the same shape the bench measures. Park-Miller LCG keeps it deterministic. @@ -46,14 +46,14 @@ local iters = tonumber(arg[1]) or 500000 -- Warmup so JIT traces compile before perf starts sampling steady state. for _ = 1, 1000 do - local d = qd.parse(payload) + local d = qjson.parse(payload) local _ = d:get_str("model") end io.stderr:write(string.format("probe: %d bytes payload, %d iters\n", #payload, iters)) for _ = 1, iters do - local d = qd.parse(payload) + local d = qjson.parse(payload) local _ = d:get_str("model") local _ = d:get_f64("temperature") local _ = d:get_str("messages[0].role") diff --git a/docs/benchmarks.md b/docs/benchmarks.md index ef0dab9..471cef8 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -1,10 +1,10 @@ # Benchmarks -Throughput and memory comparison of `quickdecode` (this library) against +Throughput and memory comparison of `qjson` (this library) against `lua-cjson` and `lua-resty-simdjson` on a multimodal chat-completion payload ladder from 2 KB to 10 MB. -`quickdecode` is optimized for *parse + read a small part of the document*; +`qjson` is optimized for *parse + read a small part of the document*; the data below quantifies how the lazy structural scan behaves when the caller reads request metadata plus every chat message `content`, without eagerly building the whole Lua table. `lua-cjson` and `lua-resty-simdjson` are eager @@ -18,7 +18,7 @@ Lua-table baselines. | Memory | 15 GiB | | OS | Ubuntu 24.04.4 LTS, Linux 6.8.0-110-generic, x86_64 | | Runtime | OpenResty `resty` 0.29 / OpenResty 1.21.4.4 / LuaJIT 2.1.1723681758 | -| `quickdecode` | this repo, release build, AVX2 + PCLMUL scanner active | +| `qjson` | this repo, release build, AVX2 + PCLMUL scanner active | | `lua-cjson` | vendored `openresty/lua-cjson` | | `lua-resty-simdjson` | `Kong/lua-resty-simdjson` commit `77322db640927c14968f1314a9fb1bb2bc084015`, installed under OpenResty lualib | @@ -27,7 +27,7 @@ Lua-table baselines. The harness lives at `benches/lua_bench.lua`. For each scenario: 1. Warmup pass (≥ 3 iterations, or `iters / 5`) to let LuaJIT compile hot - traces and the `quickdecode` `indices` / `scratch` buffers grow to their + traces and the `qjson` `indices` / `scratch` buffers grow to their working size. Warmup is excluded from timing and the memory delta. 2. `collectgarbage("collect")` baseline. 3. 5 rounds × N iterations of the workload; report the **median** ops/s @@ -55,9 +55,9 @@ parsing workloads with ~3-5% structural density. |---|---|---| | `cjson.decode + access fields` | `cjson.decode(s)`, read `model` / `temperature`, then read every `messages[*].content` | Eager Lua table | | `simdjson.decode + access fields` | `resty.simdjson:decode(s)`, read `model` / `temperature`, then read every `messages[*].content` | Eager Lua table | -| `quickdecode.parse + access fields` | `qd.parse(s)`, read `model` / `temperature`, then touch every `messages[*].content` path | Lazy structural scan; explicit path reads | -| `qd.decode + access content` | `qd.decode(s)`, read `model` / `temperature`, then read every `messages[*].content` | Lazy table proxy; reads go through `__index` | -| `qd.decode + qd.encode (unmodified)` | `qd.decode(s)` then re-emit as JSON | Substring fast path — no fields touched, so the proxy re-emits the original byte range via `memcpy` | +| `qjson.parse + access fields` | `qjson.parse(s)`, read `model` / `temperature`, then touch every `messages[*].content` path | Lazy structural scan; explicit path reads | +| `qjson.decode + access content` | `qjson.decode(s)`, read `model` / `temperature`, then read every `messages[*].content` | Lazy table proxy; reads go through `__index` | +| `qjson.decode + qjson.encode (unmodified)` | `qjson.decode(s)` then re-emit as JSON | Substring fast path — no fields touched, so the proxy re-emits the original byte range via `memcpy` | ## Reproducing @@ -67,7 +67,7 @@ Run the full comparison with one command: make bench ``` -This builds `quickdecode`, builds the vendored `lua-cjson` against OpenResty's +This builds `qjson`, builds the vendored `lua-cjson` against OpenResty's LuaJIT, then invokes `benches/lua_bench.lua` through OpenResty's `resty` so `lua-resty-simdjson` runs in its normal `ngx` environment. If `resty.simdjson` is not available on `package.path` / `package.cpath`, the @@ -79,7 +79,7 @@ Numbers below come from one such run. Each row is "parse + access request fields" on the named payload. -| Scenario | Size | cjson | simdjson | `qd.parse` | `qd.decode + access content` | `qd.decode + qd.encode` | +| Scenario | Size | cjson | simdjson | `qjson.parse` | `qjson.decode + access content` | `qjson.decode + qjson.encode` | |---|---:|---:|---:|---:|---:|---:| | small | 2.1 KB | 106,646 | 137,427 | 135,296 | 97,574 | 202,388 | | medium | 60.4 KB | 10,086 | 86,029 | 189,970 | 198,098 | 175,562 | @@ -95,7 +95,7 @@ Each row is "parse + access request fields" on the named payload. ### Speed-up vs. baselines -| Scenario | `qd.parse` / cjson | `qd.parse` / simdjson | `qd.decode + access content` / cjson | `qd.decode + access content` / simdjson | +| Scenario | `qjson.parse` / cjson | `qjson.parse` / simdjson | `qjson.decode + access content` / cjson | `qjson.decode + access content` / simdjson | |---|---:|---:|---:|---:| | small | 1.3× | 1.0× | 0.9× | 0.7× | | medium | 18.8× | 2.2× | 19.6× | 2.3× | @@ -114,7 +114,7 @@ Post-run `collectgarbage("count")` minus baseline. Captures heap usage after the timing rounds without forcing a final collection, so short-lived garbage from the last round may still be included. -| Scenario | cjson | simdjson | `qd.parse` | `qd.decode + access content` | `qd.decode + qd.encode` | +| Scenario | cjson | simdjson | `qjson.parse` | `qjson.decode + access content` | `qjson.decode + qjson.encode` | |---|---:|---:|---:|---:|---:| | small | +15,464 | +15,447 | +4,094 | +15,251 | +11,908 | | medium | +1,955 | +2,660 | +160 | +1,210 | +1,216 | @@ -128,16 +128,16 @@ from the last round may still be included. | 10m | +1,583 | +2,014 | +16 | +844 | +48 | | interleaved | +3,355 | +4,404 | +314 | +2,825 | +945 | -`qd.parse` retention is essentially constant across payload size: the only +`qjson.parse` retention is essentially constant across payload size: the only GC-rooted state is the reusable `indices: Vec` and `scratch` buffers. -The `qd.decode + ...` paths retain a bit more — a few Lua tables for the +The `qjson.decode + ...` paths retain a bit more — a few Lua tables for the lazy proxy and any cached child views — but still allocate one to two orders of magnitude less than the eager parsers, which materialize every key into the Lua table heap. ## Observations -1. **`quickdecode` is fastest once payloads move beyond tiny inputs.** +1. **`qjson` is fastest once payloads move beyond tiny inputs.** The small 2 KB row is dominated by fixed Lua/FFI overhead, but medium and larger multimodal payloads show roughly 18–28× higher throughput than `cjson` and roughly 3–5× higher throughput than `lua-resty-simdjson` @@ -146,20 +146,20 @@ key into the Lua table heap. multimodal bodies.** The benchmark touches the top-level request fields and one `content` field per message; the payload size comes from image data inside each message. -3. **The win drops at 10 MB.** `qd.parse` is L3-bandwidth-bound at that - size, and the `qd.decode` proxy's per-`__index` dispatch starts to +3. **The win drops at 10 MB.** `qjson.parse` is L3-bandwidth-bound at that + size, and the `qjson.decode` proxy's per-`__index` dispatch starts to amortize less well against the cheaper structural scan. `cjson` is still allocating into the table heap at that size, so the ratio remains large. -4. **`qd.decode + qd.encode (unmodified)` is the headline number for +4. **`qjson.decode + qjson.encode (unmodified)` is the headline number for passthrough workloads** — e.g. an LLM gateway re-emitting the original JSON after light-touch inspection. The substring fast path means re-emit is `memcpy`, not re-serialize, and the throughput tracks - `qd.parse` very closely. -5. **Memory retention** for `quickdecode` is essentially flat in payload + `qjson.parse` very closely. +5. **Memory retention** for `qjson` is essentially flat in payload size; the eager parsers retain more Lua heap after the first run because the Lua table tree stays GC-rooted until the next collection. The 10 MB case retains ~1.5 MB for `cjson`, ~2.0 MB for simdjson, - and ~16 KB for `qd.parse`. + and ~16 KB for `qjson.parse`. 6. **REST API payloads (github-100k) show a smaller speedup** because their structural density is higher than the multimodal request ladder. Memory savings remain dramatic because `cjson` must materialize every nested @@ -168,13 +168,13 @@ key into the Lua table heap. ## When to pick which - **Read most/all fields** → `cjson`. -- **Parse, read selected fields, discard / re-emit** → `quickdecode`. The +- **Parse, read selected fields, discard / re-emit** → `qjson`. The bigger the payload and the smaller the read fraction, the larger the - win. `qd.decode` / `qd.encode` gives a `cjson`-shaped surface; `qd.parse` + win. `qjson.decode` / `qjson.encode` gives a `cjson`-shaped surface; `qjson.parse` + path getters is the lower-level API with slightly higher peak throughput on the access-light workloads. -- **Round-trip / passthrough an unmodified JSON** → `qd.decode + - qd.encode`. Re-emit is `memcpy` for any subtree the caller did not +- **Round-trip / passthrough an unmodified JSON** → `qjson.decode + + qjson.encode`. Re-emit is `memcpy` for any subtree the caller did not touch. ## Caveats @@ -185,7 +185,7 @@ key into the Lua table heap. parts). Object-key-heavy JSON shifts the picture: more structural work per byte and less raw `memcpy`, while the table-build cost on the eager side rises. -- `quickdecode` retains the source buffer on the `Doc`, so the input +- `qjson` retains the source buffer on the `Doc`, so the input string stays alive for the document's lifetime. If you parse and immediately discard the JSON string in the caller, GC can still free the input — but only after the `Doc` is also unreachable. diff --git a/docs/rfc8259-conformance.md b/docs/rfc8259-conformance.md index b203d84..474109c 100644 --- a/docs/rfc8259-conformance.md +++ b/docs/rfc8259-conformance.md @@ -1,16 +1,18 @@ # RFC 8259 conformance: implementation-defined cases JSONTestSuite categorizes some inputs as `i_*` — the spec allows either -acceptance or rejection. This file records `lua-quick-decode`'s behavior on +acceptance or rejection. This file records `qjson`'s behavior on each, so changes show up in `git diff`. Behavior is recorded for the default **EAGER** mode unless noted. | File pattern | Our verdict | Rationale | |---|---|---| -| `i_number_huge_exp` | REJECT (`QJD_NUMBER_OUT_OF_RANGE`) | f64 overflow surfaces at decode. | -| `i_number_very_big_negative_int` | varies — see below | ABNF-valid; representational, not structural. | -| `i_string_*` (UTF-16 surrogate halves in `\u` escapes) | REJECT (`QJD_DECODE_FAILED`) | We require well-formed surrogate pairs. | +| `i_number_*` | ACCEPT | Eager validation checks JSON number grammar, not numeric representability. Overflow can still surface during typed access. | +| `i_object_key_lone_2nd_surrogate`, `i_string_*surrogate*` escaped with `\u` | ACCEPT | Eager parse validates escape syntax. Unicode scalar decoding is deferred until typed string access. | +| `i_string_*invalid*`, `i_string_*overlong*`, `i_string_*utf-8*`, `i_string_iso_latin_1`, `i_string_lone_utf8_continuation_byte`, `i_string_not_in_unicode_range` | REJECT (`QJSON_INVALID_UTF8`) | Raw string bytes must be valid UTF-8. | +| `i_string_UTF-16LE_with_BOM`, `i_string_utf16LE_no_BOM` | REJECT (`QJSON_TRAILING_CONTENT`) | UTF-16 input is outside the supported UTF-8 JSON text encoding. | +| `i_string_utf16BE_no_BOM`, `i_structure_UTF-8_BOM_empty_object` | REJECT (`QJSON_PARSE_ERROR`) | UTF-16 and leading BOM inputs are outside the accepted JSON text form. | | `i_structure_500_nested_arrays` | ACCEPT (within default 1024 max_depth) | Configurable. | Run `cargo test --release --test json_test_suite -- --nocapture` to print the diff --git a/include/lua_quick_decode.h b/include/lua_quick_decode.h deleted file mode 100644 index f920ab1..0000000 --- a/include/lua_quick_decode.h +++ /dev/null @@ -1,90 +0,0 @@ -#ifndef LUA_QUICK_DECODE_H -#define LUA_QUICK_DECODE_H - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -typedef enum { - QJD_OK = 0, - QJD_PARSE_ERROR = 1, - QJD_NOT_FOUND = 2, - QJD_TYPE_MISMATCH = 3, - QJD_OUT_OF_RANGE = 4, - QJD_DECODE_FAILED = 5, - QJD_INVALID_PATH = 6, - QJD_INVALID_ARG = 7, - QJD_OOM = 8, - QJD_NESTING_TOO_DEEP = 9, - QJD_TRAILING_CONTENT = 10, - QJD_NUMBER_OUT_OF_RANGE = 11, - QJD_INVALID_NUMBER = 12, - QJD_INVALID_STRING = 13, - QJD_INVALID_UTF8 = 14 -} qjd_err; - -typedef enum { - QJD_T_NULL = 0, QJD_T_BOOL = 1, QJD_T_NUM = 2, - QJD_T_STR = 3, QJD_T_ARR = 4, QJD_T_OBJ = 5 -} qjd_type; - -#define QJD_MODE_EAGER 0u -#define QJD_MODE_LAZY 1u -#define QJD_DEFAULT_MAX_DEPTH 1024u - -typedef struct { - uint32_t mode; /* QJD_MODE_EAGER (0) or QJD_MODE_LAZY (1) */ - uint32_t max_depth; /* 0 = use QJD_DEFAULT_MAX_DEPTH */ -} qjd_options; - -typedef struct qjd_doc qjd_doc; - -typedef struct { - const qjd_doc* doc; - uint32_t idx_start; - uint32_t idx_end; - uint32_t _reserved0; - uint32_t _reserved1; -} qjd_cursor; - -const char* qjd_strerror(int code); - -qjd_doc* qjd_parse(const uint8_t* buf, size_t len, int* err_out); -qjd_doc* qjd_parse_ex(const uint8_t* buf, size_t len, - const qjd_options* opts, int* err_out); -void qjd_free (qjd_doc* doc); - -int qjd_get_str (qjd_doc*, const char* path, size_t path_len, - const uint8_t** out_ptr, size_t* out_len); -int qjd_get_i64 (qjd_doc*, const char* path, size_t path_len, int64_t* out); -int qjd_get_f64 (qjd_doc*, const char* path, size_t path_len, double* out); -int qjd_get_bool (qjd_doc*, const char* path, size_t path_len, int* out); -int qjd_is_null (qjd_doc*, const char* path, size_t path_len, int* out); -int qjd_typeof (qjd_doc*, const char* path, size_t path_len, int* type_out); -int qjd_len (qjd_doc*, const char* path, size_t path_len, size_t* out); - -int qjd_open (qjd_doc*, const char* path, size_t path_len, qjd_cursor* out); -int qjd_cursor_open (const qjd_cursor*, const char* path, size_t path_len, qjd_cursor* out); -int qjd_cursor_field (const qjd_cursor*, const char* key, size_t key_len, qjd_cursor* out); -int qjd_cursor_index (const qjd_cursor*, size_t i, qjd_cursor* out); - -int qjd_cursor_get_str (const qjd_cursor*, const char* path, size_t path_len, - const uint8_t** out_ptr, size_t* out_len); -int qjd_cursor_get_i64 (const qjd_cursor*, const char* path, size_t path_len, int64_t* out); -int qjd_cursor_get_f64 (const qjd_cursor*, const char* path, size_t path_len, double* out); -int qjd_cursor_get_bool (const qjd_cursor*, const char* path, size_t path_len, int* out); -int qjd_cursor_typeof (const qjd_cursor*, const char* path, size_t path_len, int* out); -int qjd_cursor_len (const qjd_cursor*, const char* path, size_t path_len, size_t* out); -int qjd_cursor_bytes (const qjd_cursor*, size_t* byte_start, size_t* byte_end); -int qjd_cursor_object_entry_at(const qjd_cursor*, size_t i, - const uint8_t** key_ptr, size_t* key_len, - qjd_cursor* value_out); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/include/qjson.h b/include/qjson.h new file mode 100644 index 0000000..343e782 --- /dev/null +++ b/include/qjson.h @@ -0,0 +1,91 @@ +#ifndef QJSON_H +#define QJSON_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + QJSON_OK = 0, + QJSON_PARSE_ERROR = 1, + QJSON_NOT_FOUND = 2, + QJSON_TYPE_MISMATCH = 3, + QJSON_OUT_OF_RANGE = 4, + QJSON_DECODE_FAILED = 5, + QJSON_INVALID_PATH = 6, + QJSON_INVALID_ARG = 7, + QJSON_OOM = 8, + QJSON_NESTING_TOO_DEEP = 9, + QJSON_TRAILING_CONTENT = 10, + QJSON_NUMBER_OUT_OF_RANGE = 11, + QJSON_INVALID_NUMBER = 12, + QJSON_INVALID_STRING = 13, + QJSON_INVALID_UTF8 = 14 +} qjson_err; + +typedef enum { + QJSON_T_NULL = 0, QJSON_T_BOOL = 1, QJSON_T_NUM = 2, + QJSON_T_STR = 3, QJSON_T_ARR = 4, QJSON_T_OBJ = 5 +} qjson_type; + +#define QJSON_MODE_EAGER 0u +#define QJSON_MODE_LAZY 1u +#define QJSON_DEFAULT_MAX_DEPTH 1024u +#define QJSON_MAX_MAX_DEPTH 4096u + +typedef struct { + uint32_t mode; /* QJSON_MODE_EAGER (0) or QJSON_MODE_LAZY (1) */ + uint32_t max_depth; /* 0 = default; values above QJSON_MAX_MAX_DEPTH are clamped */ +} qjson_options; + +typedef struct qjson_doc qjson_doc; + +typedef struct { + const qjson_doc* doc; + uint32_t idx_start; + uint32_t idx_end; + uint32_t _reserved0; + uint32_t _reserved1; +} qjson_cursor; + +const char* qjson_strerror(int code); + +qjson_doc* qjson_parse(const uint8_t* buf, size_t len, int* err_out); +qjson_doc* qjson_parse_ex(const uint8_t* buf, size_t len, + const qjson_options* opts, int* err_out); +void qjson_free (qjson_doc* doc); + +int qjson_get_str (qjson_doc*, const char* path, size_t path_len, + const uint8_t** out_ptr, size_t* out_len); +int qjson_get_i64 (qjson_doc*, const char* path, size_t path_len, int64_t* out); +int qjson_get_f64 (qjson_doc*, const char* path, size_t path_len, double* out); +int qjson_get_bool (qjson_doc*, const char* path, size_t path_len, int* out); +int qjson_is_null (qjson_doc*, const char* path, size_t path_len, int* out); +int qjson_typeof (qjson_doc*, const char* path, size_t path_len, int* type_out); +int qjson_len (qjson_doc*, const char* path, size_t path_len, size_t* out); + +int qjson_open (qjson_doc*, const char* path, size_t path_len, qjson_cursor* out); +int qjson_cursor_open (const qjson_cursor*, const char* path, size_t path_len, qjson_cursor* out); +int qjson_cursor_field (const qjson_cursor*, const char* key, size_t key_len, qjson_cursor* out); +int qjson_cursor_index (const qjson_cursor*, size_t i, qjson_cursor* out); + +int qjson_cursor_get_str (const qjson_cursor*, const char* path, size_t path_len, + const uint8_t** out_ptr, size_t* out_len); +int qjson_cursor_get_i64 (const qjson_cursor*, const char* path, size_t path_len, int64_t* out); +int qjson_cursor_get_f64 (const qjson_cursor*, const char* path, size_t path_len, double* out); +int qjson_cursor_get_bool (const qjson_cursor*, const char* path, size_t path_len, int* out); +int qjson_cursor_typeof (const qjson_cursor*, const char* path, size_t path_len, int* out); +int qjson_cursor_len (const qjson_cursor*, const char* path, size_t path_len, size_t* out); +int qjson_cursor_bytes (const qjson_cursor*, size_t* byte_start, size_t* byte_end); +int qjson_cursor_object_entry_at(const qjson_cursor*, size_t i, + const uint8_t** key_ptr, size_t* key_len, + qjson_cursor* value_out); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/lua/quickdecode.lua b/lua/qjson.lua similarity index 53% rename from lua/quickdecode.lua rename to lua/qjson.lua index 5ab6c5f..3e57324 100644 --- a/lua/quickdecode.lua +++ b/lua/qjson.lua @@ -1,49 +1,49 @@ local ffi = require("ffi") ffi.cdef[[ -typedef struct qjd_doc qjd_doc; +typedef struct qjson_doc qjson_doc; typedef struct { - const qjd_doc* doc; + const qjson_doc* doc; uint32_t idx_start, idx_end, _reserved0, _reserved1; -} qjd_cursor; +} qjson_cursor; typedef struct { uint32_t mode; uint32_t max_depth; -} qjd_options; - -const char* qjd_strerror(int code); -qjd_doc* qjd_parse (const uint8_t* buf, size_t len, int* err_out); -qjd_doc* qjd_parse_ex(const uint8_t* buf, size_t len, - const qjd_options* opts, int* err_out); -void qjd_free (qjd_doc* doc); - -int qjd_get_str (qjd_doc*, const char* path, size_t path_len, const uint8_t** p, size_t* n); -int qjd_get_i64 (qjd_doc*, const char* path, size_t path_len, int64_t* out); -int qjd_get_f64 (qjd_doc*, const char* path, size_t path_len, double* out); -int qjd_get_bool(qjd_doc*, const char* path, size_t path_len, int* out); -int qjd_is_null (qjd_doc*, const char* path, size_t path_len, int* out); -int qjd_typeof (qjd_doc*, const char* path, size_t path_len, int* out); -int qjd_len (qjd_doc*, const char* path, size_t path_len, size_t* out); - -int qjd_open (qjd_doc*, const char* path, size_t path_len, qjd_cursor* out); -int qjd_cursor_open (const qjd_cursor*, const char* path, size_t path_len, qjd_cursor* out); -int qjd_cursor_field(const qjd_cursor*, const char* key, size_t key_len, qjd_cursor* out); -int qjd_cursor_index(const qjd_cursor*, size_t i, qjd_cursor* out); - -int qjd_cursor_get_str (const qjd_cursor*, const char*, size_t, const uint8_t**, size_t*); -int qjd_cursor_get_i64 (const qjd_cursor*, const char*, size_t, int64_t*); -int qjd_cursor_get_f64 (const qjd_cursor*, const char*, size_t, double*); -int qjd_cursor_get_bool(const qjd_cursor*, const char*, size_t, int*); -int qjd_cursor_typeof (const qjd_cursor*, const char*, size_t, int*); -int qjd_cursor_len (const qjd_cursor*, const char*, size_t, size_t*); -int qjd_cursor_bytes(const qjd_cursor*, size_t* byte_start, size_t* byte_end); -int qjd_cursor_object_entry_at(const qjd_cursor*, size_t i, +} qjson_options; + +const char* qjson_strerror(int code); +qjson_doc* qjson_parse (const uint8_t* buf, size_t len, int* err_out); +qjson_doc* qjson_parse_ex(const uint8_t* buf, size_t len, + const qjson_options* opts, int* err_out); +void qjson_free (qjson_doc* doc); + +int qjson_get_str (qjson_doc*, const char* path, size_t path_len, const uint8_t** p, size_t* n); +int qjson_get_i64 (qjson_doc*, const char* path, size_t path_len, int64_t* out); +int qjson_get_f64 (qjson_doc*, const char* path, size_t path_len, double* out); +int qjson_get_bool(qjson_doc*, const char* path, size_t path_len, int* out); +int qjson_is_null (qjson_doc*, const char* path, size_t path_len, int* out); +int qjson_typeof (qjson_doc*, const char* path, size_t path_len, int* out); +int qjson_len (qjson_doc*, const char* path, size_t path_len, size_t* out); + +int qjson_open (qjson_doc*, const char* path, size_t path_len, qjson_cursor* out); +int qjson_cursor_open (const qjson_cursor*, const char* path, size_t path_len, qjson_cursor* out); +int qjson_cursor_field(const qjson_cursor*, const char* key, size_t key_len, qjson_cursor* out); +int qjson_cursor_index(const qjson_cursor*, size_t i, qjson_cursor* out); + +int qjson_cursor_get_str (const qjson_cursor*, const char*, size_t, const uint8_t**, size_t*); +int qjson_cursor_get_i64 (const qjson_cursor*, const char*, size_t, int64_t*); +int qjson_cursor_get_f64 (const qjson_cursor*, const char*, size_t, double*); +int qjson_cursor_get_bool(const qjson_cursor*, const char*, size_t, int*); +int qjson_cursor_typeof (const qjson_cursor*, const char*, size_t, int*); +int qjson_cursor_len (const qjson_cursor*, const char*, size_t, size_t*); +int qjson_cursor_bytes(const qjson_cursor*, size_t* byte_start, size_t* byte_end); +int qjson_cursor_object_entry_at(const qjson_cursor*, size_t i, const uint8_t** key_ptr, size_t* key_len, - qjd_cursor* value_out); + qjson_cursor* value_out); ]] -local C = ffi.load("quickdecode") +local C = ffi.load("qjson") local err_box = ffi.new("int[1]") local i64_box = ffi.new("int64_t[1]") @@ -52,10 +52,10 @@ local bool_box = ffi.new("int[1]") local size_box = ffi.new("size_t[1]") local type_box = ffi.new("int[1]") local strp_box = ffi.new("const uint8_t*[1]") -local cur_box = ffi.new("qjd_cursor[1]") +local cur_box = ffi.new("qjson_cursor[1]") local NOT_FOUND = 2 --- Error codes mirrored from include/lua_quick_decode.h. Kept in sync manually; +-- Error codes mirrored from include/qjson.h. Kept in sync manually; -- src/error.rs has the authoritative numbering. local ERR = { OK = 0, @@ -87,10 +87,10 @@ local Cursor = {}; Cursor.__index = Cursor local function check_err(rc) if rc == 0 then return true end if rc == NOT_FOUND then return false end - error("quickdecode: " .. ffi.string(C.qjd_strerror(rc))) + error("qjson: " .. ffi.string(C.qjson_strerror(rc))) end -local opts_box = ffi.new("qjd_options[1]") +local opts_box = ffi.new("qjson_options[1]") local MODE_EAGER = 0 local MODE_LAZY = 1 @@ -98,142 +98,142 @@ local MODE_LAZY = 1 function _M.parse(json_str, opts) local ptr if opts == nil then - ptr = C.qjd_parse(json_str, #json_str, err_box) + ptr = C.qjson_parse(json_str, #json_str, err_box) else if type(opts) ~= "table" then - error("quickdecode.parse: opts must be a table") + error("qjson.parse: opts must be a table") end local lazy = opts.lazy if lazy ~= nil and type(lazy) ~= "boolean" then - error("quickdecode.parse: opts.lazy must be a boolean") + error("qjson.parse: opts.lazy must be a boolean") end local max_depth = opts.max_depth or 0 if type(max_depth) ~= "number" or max_depth < 0 or max_depth ~= math.floor(max_depth) then - error("quickdecode.parse: opts.max_depth must be a non-negative integer") + error("qjson.parse: opts.max_depth must be a non-negative integer") end opts_box[0].mode = lazy and MODE_LAZY or MODE_EAGER opts_box[0].max_depth = max_depth - ptr = C.qjd_parse_ex(json_str, #json_str, opts_box, err_box) + ptr = C.qjson_parse_ex(json_str, #json_str, opts_box, err_box) end if ptr == nil then - error("quickdecode: " .. ffi.string(C.qjd_strerror(err_box[0]))) + error("qjson: " .. ffi.string(C.qjson_strerror(err_box[0]))) end return setmetatable({ - _ptr = ffi.gc(ptr, C.qjd_free), + _ptr = ffi.gc(ptr, C.qjson_free), _hold = json_str, -- strong ref keeps buffer alive }, Doc) end function Doc:get_str(path) - local rc = C.qjd_get_str(self._ptr, path, #path, strp_box, size_box) + local rc = C.qjson_get_str(self._ptr, path, #path, strp_box, size_box) if not check_err(rc) then return nil end return ffi.string(strp_box[0], size_box[0]) end function Doc:get_i64(path) - local rc = C.qjd_get_i64(self._ptr, path, #path, i64_box) + local rc = C.qjson_get_i64(self._ptr, path, #path, i64_box) if not check_err(rc) then return nil end return tonumber(i64_box[0]) end function Doc:get_f64(path) - local rc = C.qjd_get_f64(self._ptr, path, #path, f64_box) + local rc = C.qjson_get_f64(self._ptr, path, #path, f64_box) if not check_err(rc) then return nil end return f64_box[0] end function Doc:get_bool(path) - local rc = C.qjd_get_bool(self._ptr, path, #path, bool_box) + local rc = C.qjson_get_bool(self._ptr, path, #path, bool_box) if not check_err(rc) then return nil end return bool_box[0] ~= 0 end function Doc:is_null(path) - local rc = C.qjd_is_null(self._ptr, path, #path, bool_box) + local rc = C.qjson_is_null(self._ptr, path, #path, bool_box) if not check_err(rc) then return nil end return bool_box[0] ~= 0 end function Doc:typeof(path) - local rc = C.qjd_typeof(self._ptr, path, #path, type_box) + local rc = C.qjson_typeof(self._ptr, path, #path, type_box) if not check_err(rc) then return nil end return type_box[0] end function Doc:len(path) - local rc = C.qjd_len(self._ptr, path, #path, size_box) + local rc = C.qjson_len(self._ptr, path, #path, size_box) if not check_err(rc) then return nil end return tonumber(size_box[0]) end function Doc:open(path) - local rc = C.qjd_open(self._ptr, path, #path, cur_box) + local rc = C.qjson_open(self._ptr, path, #path, cur_box) if not check_err(rc) then return nil end return setmetatable({ _cur = cur_box[0], _doc = self }, Cursor) end function Cursor:get_str(path) path = path or "" - local rc = C.qjd_cursor_get_str(self._cur, path, #path, strp_box, size_box) + local rc = C.qjson_cursor_get_str(self._cur, path, #path, strp_box, size_box) if not check_err(rc) then return nil end return ffi.string(strp_box[0], size_box[0]) end function Cursor:get_i64(path) path = path or "" - local rc = C.qjd_cursor_get_i64(self._cur, path, #path, i64_box) + local rc = C.qjson_cursor_get_i64(self._cur, path, #path, i64_box) if not check_err(rc) then return nil end return tonumber(i64_box[0]) end function Cursor:get_f64(path) path = path or "" - local rc = C.qjd_cursor_get_f64(self._cur, path, #path, f64_box) + local rc = C.qjson_cursor_get_f64(self._cur, path, #path, f64_box) if not check_err(rc) then return nil end return f64_box[0] end function Cursor:get_bool(path) path = path or "" - local rc = C.qjd_cursor_get_bool(self._cur, path, #path, bool_box) + local rc = C.qjson_cursor_get_bool(self._cur, path, #path, bool_box) if not check_err(rc) then return nil end return bool_box[0] ~= 0 end function Cursor:typeof(path) path = path or "" - local rc = C.qjd_cursor_typeof(self._cur, path, #path, type_box) + local rc = C.qjson_cursor_typeof(self._cur, path, #path, type_box) if not check_err(rc) then return nil end return type_box[0] end function Cursor:len(path) path = path or "" - local rc = C.qjd_cursor_len(self._cur, path, #path, size_box) + local rc = C.qjson_cursor_len(self._cur, path, #path, size_box) if not check_err(rc) then return nil end return tonumber(size_box[0]) end function Cursor:open(path) - local rc = C.qjd_cursor_open(self._cur, path, #path, cur_box) + local rc = C.qjson_cursor_open(self._cur, path, #path, cur_box) if not check_err(rc) then return nil end return setmetatable({ _cur = cur_box[0], _doc = self._doc }, Cursor) end function Cursor:field(key) - local rc = C.qjd_cursor_field(self._cur, key, #key, cur_box) + local rc = C.qjson_cursor_field(self._cur, key, #key, cur_box) if not check_err(rc) then return nil end return setmetatable({ _cur = cur_box[0], _doc = self._doc }, Cursor) end function Cursor:index(i) - local rc = C.qjd_cursor_index(self._cur, i, cur_box) + local rc = C.qjson_cursor_index(self._cur, i, cur_box) if not check_err(rc) then return nil end return setmetatable({ _cur = cur_box[0], _doc = self._doc }, Cursor) end --- Lazy table API (cjson-shaped surface). See lua/quickdecode/table.lua. -local _lazy = require("quickdecode.table") +-- Lazy table API (cjson-shaped surface). See lua/qjson/table.lua. +local _lazy = require("qjson.table") _M.decode = _lazy.decode _M.encode = _lazy.encode _M.materialize = _lazy.materialize diff --git a/lua/quickdecode/table.lua b/lua/qjson/table.lua similarity index 83% rename from lua/quickdecode/table.lua rename to lua/qjson/table.lua index 9a8b93d..f23c076 100644 --- a/lua/quickdecode/table.lua +++ b/lua/qjson/table.lua @@ -1,16 +1,16 @@ --- Lazy table view + cjson-compatible encoder for quickdecode. +-- Lazy table view + cjson-compatible encoder for qjson. -- --- This module relies on the FFI cdef set up by `lua/quickdecode.lua`, so --- callers must `require("quickdecode")` (transitively or directly) before +-- This module relies on the FFI cdef set up by `lua/qjson.lua`, so +-- callers must `require("qjson")` (transitively or directly) before -- they require this module. local ffi = require("ffi") -local C = ffi.load("quickdecode") --- Defer the require to avoid a circular dependency when quickdecode.lua --- re-exports this module. By the time _M.decode is called, quickdecode +local C = ffi.load("qjson") +-- Defer the require to avoid a circular dependency when qjson.lua +-- re-exports this module. By the time _M.decode is called, qjson -- is already registered in package.loaded. -local function get_qd() - return require("quickdecode") +local function get_qjson() + return require("qjson") end -- Optional cjson bridge: reuse its sentinels when available so callers' @@ -37,13 +37,13 @@ local bool_box = ffi.new("int[1]") local size_box = ffi.new("size_t[1]") local type_box = ffi.new("int[1]") local strp_box = ffi.new("const uint8_t*[1]") -local cur_box = ffi.new("qjd_cursor[1]") -local child_box = ffi.new("qjd_cursor[1]") +local cur_box = ffi.new("qjson_cursor[1]") +local child_box = ffi.new("qjson_cursor[1]") local sz_a = ffi.new("size_t[1]") local sz_b = ffi.new("size_t[1]") -local QJD_OK = 0 -local QJD_NOT_FOUND = 2 +local QJSON_OK = 0 +local QJSON_NOT_FOUND = 2 local T_NULL = 0 local T_BOOL = 1 local T_NUM = 2 @@ -52,22 +52,22 @@ local T_ARR = 4 local T_OBJ = 5 local function check(rc) - if rc == QJD_OK then return true end - if rc == QJD_NOT_FOUND then return false end - error("quickdecode: " .. ffi.string(C.qjd_strerror(rc))) + if rc == QJSON_OK then return true end + if rc == QJSON_NOT_FOUND then return false end + error("qjson: " .. ffi.string(C.qjson_strerror(rc))) end local LazyObject = {} local LazyArray = {} -- Build a new lazy view for a child container cursor. --- src_box is an FFI cdata `qjd_cursor[1]`; src_box[0] is the cursor whose +-- src_box is an FFI cdata `qjson_cursor[1]`; src_box[0] is the cursor whose -- data we copy into a fresh per-view allocation so the new view's _cur -- survives later overwrites of src_box. local function wrap_child(parent_view, src_box) - C.qjd_cursor_bytes(src_box[0], sz_a, sz_b) - local own_box = ffi.new("qjd_cursor[1]") - ffi.copy(own_box, src_box, ffi.sizeof("qjd_cursor")) + C.qjson_cursor_bytes(src_box[0], sz_a, sz_b) + local own_box = ffi.new("qjson_cursor[1]") + ffi.copy(own_box, src_box, ffi.sizeof("qjson_cursor")) return { _doc = parent_view._doc, _cur_box = own_box, -- keep cdata alive @@ -78,22 +78,22 @@ local function wrap_child(parent_view, src_box) end -- Decode the value at src_box[0] into a Lua value. --- src_box is a `qjd_cursor[1]`; for container types, a new view is created +-- src_box is a `qjson_cursor[1]`; for container types, a new view is created -- via wrap_child so the caller's box can be freely reused afterwards. local function decode_cursor(parent_view, src_box) - local trc = C.qjd_cursor_typeof(src_box[0], "", 0, type_box) + local trc = C.qjson_cursor_typeof(src_box[0], "", 0, type_box) if not check(trc) then return nil end local t = type_box[0] if t == T_STR then - local rrc = C.qjd_cursor_get_str(src_box[0], "", 0, strp_box, size_box) + local rrc = C.qjson_cursor_get_str(src_box[0], "", 0, strp_box, size_box) if not check(rrc) then return nil end return ffi.string(strp_box[0], size_box[0]) elseif t == T_NUM then - local rrc = C.qjd_cursor_get_f64(src_box[0], "", 0, f64_box) + local rrc = C.qjson_cursor_get_f64(src_box[0], "", 0, f64_box) if not check(rrc) then return nil end return f64_box[0] elseif t == T_BOOL then - local rrc = C.qjd_cursor_get_bool(src_box[0], "", 0, bool_box) + local rrc = C.qjson_cursor_get_bool(src_box[0], "", 0, bool_box) if not check(rrc) then return nil end return bool_box[0] ~= 0 elseif t == T_NULL then @@ -117,7 +117,7 @@ local function read_object_field(self, key) if type(key) ~= "string" then return nil end -- Use child_box so the lookup result does not alias self._cur (which is -- itself stored in root_box's backing memory in the decode caller). - local rc = C.qjd_cursor_field(self._cur, key, #key, child_box) + local rc = C.qjson_cursor_field(self._cur, key, #key, child_box) if not check(rc) then return nil end local v = decode_cursor(self, child_box) -- Cache containers so identity is stable and materialization sticks. @@ -136,7 +136,7 @@ local function read_array_index(self, key) -- 1-based external, 0-based internal local i = key - 1 if i < 0 or i ~= math.floor(i) then return nil end - local rc = C.qjd_cursor_index(self._cur, i, child_box) + local rc = C.qjson_cursor_index(self._cur, i, child_box) if not check(rc) then return nil end local v = decode_cursor(self, child_box) -- Cache containers so identity is stable and materialization sticks. @@ -151,10 +151,10 @@ LazyArray.__index = read_array_index local function lazy_object_iter(state, _prev_key) local i = state.i state.i = i + 1 - local rc = C.qjd_cursor_object_entry_at( + local rc = C.qjson_cursor_object_entry_at( state.view._cur, i, strp_box, size_box, child_box ) - if rc == QJD_NOT_FOUND then return nil end + if rc == QJSON_NOT_FOUND then return nil end check(rc) local k = ffi.string(strp_box[0], size_box[0]) local v = decode_cursor(state.view, child_box) @@ -167,8 +167,8 @@ end local function lazy_array_iter(state, _prev_i) local i = state.i - local rc = C.qjd_cursor_index(state.view._cur, i, child_box) - if rc == QJD_NOT_FOUND then return nil end + local rc = C.qjson_cursor_index(state.view._cur, i, child_box) + if rc == QJSON_NOT_FOUND then return nil end check(rc) state.i = i + 1 local v = decode_cursor(state.view, child_box) @@ -198,7 +198,7 @@ function _M.pairs(t) end local function lazy_len(self) - local rc = C.qjd_cursor_len(self._cur, "", 0, size_box) + local rc = C.qjson_cursor_len(self._cur, "", 0, size_box) check(rc) return tonumber(size_box[0]) end @@ -209,7 +209,7 @@ LazyArray.__len = lazy_len -- Public fallback for `#t` on a lazy proxy. Vanilla LuaJIT 5.1 does not invoke -- __len on tables (only userdata) unless built with LUAJIT_ENABLE_LUA52COMPAT -- (OpenResty's default). Callers running on a non-compat LuaJIT must use --- qt.len(t) — same role qt.pairs / qt.ipairs play for __pairs / __ipairs. +-- qjson.len(t) — same role qjson.pairs / qjson.ipairs play for __pairs / __ipairs. function _M.len(t) local mt = getmetatable(t) if mt == LazyObject or mt == LazyArray then @@ -225,8 +225,8 @@ local function materialize_object_contents(view) local i = 0 local pairs_out = {} while true do - local rc = C.qjd_cursor_object_entry_at(view._cur, i, strp_box, size_box, child_box) - if rc == QJD_NOT_FOUND then break end + local rc = C.qjson_cursor_object_entry_at(view._cur, i, strp_box, size_box, child_box) + if rc == QJSON_NOT_FOUND then break end check(rc) local k = ffi.string(strp_box[0], size_box[0]) local v = decode_cursor(view, child_box) @@ -242,8 +242,8 @@ local function materialize_array_contents(view) local i = 0 local out = {} while true do - local rc = C.qjd_cursor_index(view._cur, i, child_box) - if rc == QJD_NOT_FOUND then break end + local rc = C.qjson_cursor_index(view._cur, i, child_box) + if rc == QJSON_NOT_FOUND then break end check(rc) out[i + 1] = decode_cursor(view, child_box) i = i + 1 @@ -309,27 +309,27 @@ LazyArray.__newindex = function(t, k, v) end function _M.decode(json_str) - -- Reuse the existing qd.parse path to get a Doc with stable buffer hold. - local doc = get_qd().parse(json_str) + -- Reuse the existing qjson.parse path to get a Doc with stable buffer hold. + local doc = get_qjson().parse(json_str) -- Open the root cursor into cur_box, then copy into a dedicated box owned -- by the view so that later child lookups (which reuse child_box) do not -- alias the root cursor's backing storage. - local rc = C.qjd_open(doc._ptr, "", 0, cur_box) + local rc = C.qjson_open(doc._ptr, "", 0, cur_box) if not check(rc) then - error("quickdecode: open root failed") + error("qjson: open root failed") end - local root_box = ffi.new("qjd_cursor[1]") - ffi.copy(root_box, cur_box, ffi.sizeof("qjd_cursor")) + local root_box = ffi.new("qjson_cursor[1]") + ffi.copy(root_box, cur_box, ffi.sizeof("qjson_cursor")) -- Determine root container kind (object/array) and wrap accordingly. -- Both have meaningful byte spans for encode. - local trc = C.qjd_cursor_typeof(root_box[0], "", 0, type_box) + local trc = C.qjson_cursor_typeof(root_box[0], "", 0, type_box) if not check(trc) then - error("quickdecode: root typeof failed") + error("qjson: root typeof failed") end local rt = type_box[0] - local brc = C.qjd_cursor_bytes(root_box[0], sz_a, sz_b) + local brc = C.qjson_cursor_bytes(root_box[0], sz_a, sz_b) if not check(brc) then - error("quickdecode: root byte-span failed") + error("qjson: root byte-span failed") end local view = { _doc = doc, @@ -343,7 +343,7 @@ function _M.decode(json_str) elseif rt == T_ARR then return setmetatable(view, LazyArray) else - error("quickdecode: top-level JSON value is not an object or array") + error("qjson: top-level JSON value is not an object or array") end end @@ -396,7 +396,7 @@ end local function encode_number(n) if n ~= n or n == math.huge or n == -math.huge then - error("qd.encode: cannot encode non-finite number") + error("qjson.encode: cannot encode non-finite number") end if n == math.floor(n) and math.abs(n) < 1e15 then return string_format("%d", n) @@ -437,8 +437,8 @@ local function encode_lazy_object_walking(t) local parts = {} local i = 0 while true do - local rc = C.qjd_cursor_object_entry_at(t._cur, i, strp_box, size_box, child_box) - if rc == QJD_NOT_FOUND then break end + local rc = C.qjson_cursor_object_entry_at(t._cur, i, strp_box, size_box, child_box) + if rc == QJSON_NOT_FOUND then break end check(rc) local k = ffi.string(strp_box[0], size_box[0]) local v @@ -456,11 +456,11 @@ end local function encode_lazy_array_walking(t) local parts = {} - local rc = C.qjd_cursor_len(t._cur, "", 0, size_box) + local rc = C.qjson_cursor_len(t._cur, "", 0, size_box) check(rc) local n = tonumber(size_box[0]) for i = 0, n - 1 do - local irc = C.qjd_cursor_index(t._cur, i, child_box) + local irc = C.qjson_cursor_index(t._cur, i, child_box) check(irc) local cached = rawget(t, i + 1) local v @@ -511,7 +511,7 @@ local function encode_object(t) local parts = {} for k, v in pairs(t) do if type(k) ~= "string" then - error("qd.encode: object key must be a string, got " .. type(k)) + error("qjson.encode: object key must be a string, got " .. type(k)) end parts[#parts+1] = encode_string(k) .. ":" .. encode(v) end @@ -539,13 +539,13 @@ encode = function(v) end return encode_object(v) end - error("qd.encode: unsupported value type: " .. tv) + error("qjson.encode: unsupported value type: " .. tv) end _M.encode = encode -- Debug convenience: tostring(lazy_view) returns the original JSON bytes. --- Not the canonical encoder — callers should still use qd.encode for output. +-- Not the canonical encoder — callers should still use qjson.encode for output. LazyObject.__tostring = encode_proxy LazyArray.__tostring = encode_proxy diff --git a/src/cursor.rs b/src/cursor.rs index b2cd890..bf38d40 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -1,5 +1,5 @@ use crate::doc::Document; -use crate::error::qjd_err; +use crate::error::qjson_err; use crate::path::{PathIter, PathSeg}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -21,7 +21,7 @@ impl Cursor { Cursor { idx_start: 0, idx_end: n - 2 } } - pub(crate) fn resolve(self, doc: &Document, path: &[u8]) -> Result { + pub(crate) fn resolve(self, doc: &Document, path: &[u8]) -> Result { let mut cur = self; for seg in PathIter::new(path) { let seg = seg?; @@ -31,13 +31,13 @@ impl Cursor { } } -fn step(doc: &Document, cur: Cursor, seg: &PathSeg) -> Result { +fn step(doc: &Document, cur: Cursor, seg: &PathSeg) -> Result { // The cursor must point at a container. let opener_byte = container_opener_byte(doc, cur) - .ok_or(qjd_err::QJD_TYPE_MISMATCH)?; + .ok_or(qjson_err::QJSON_TYPE_MISMATCH)?; match (seg, opener_byte) { (PathSeg::Key(_), b'{') | (PathSeg::Idx(_), b'[') => {} - _ => return Err(qjd_err::QJD_TYPE_MISMATCH), + _ => return Err(qjson_err::QJSON_TYPE_MISMATCH), } walk_children(doc, cur, seg) @@ -55,7 +55,7 @@ fn container_opener_byte(doc: &Document, cur: Cursor) -> Option { /// Iterate children of the container at `cur` and return a Cursor for the /// matching child. Populates the skip cache on the first visit; uses it on /// subsequent visits. -fn walk_children(doc: &Document, cur: Cursor, seg: &PathSeg) -> Result { +fn walk_children(doc: &Document, cur: Cursor, seg: &PathSeg) -> Result { let is_obj = matches!(seg, PathSeg::Key(_)); let mut cache = doc.skip.borrow_mut(); let (slot_n, was_cached) = cache.get_or_insert(cur.idx_start); @@ -90,7 +90,7 @@ fn walk_children(doc: &Document, cur: Cursor, seg: &PathSeg) -> Result Result Result { i = skip_end + 1; arr_idx += 1; } b'}' | b']' => break, - _ => return Err(qjd_err::QJD_PARSE_ERROR), + _ => return Err(qjson_err::QJSON_PARSE_ERROR), } } @@ -138,13 +138,13 @@ fn walk_children(doc: &Document, cur: Cursor, seg: &PathSeg) -> Result Ok(c), - None => Err(qjd_err::QJD_NOT_FOUND), + None => Err(qjson_err::QJSON_NOT_FOUND), } } fn resolve_in_known_children( doc: &Document, starts: &[u32], ends: &[u32], is_obj: bool, seg: &PathSeg, -) -> Result { +) -> Result { for (k, (&i, &cursor_end)) in starts.iter().zip(ends.iter()).enumerate() { let matched = if is_obj { let key_open = doc.indices[i as usize] as usize; @@ -159,7 +159,7 @@ fn resolve_in_known_children( return Ok(Cursor { idx_start: value_idx_start, idx_end: cursor_end }); } } - Err(qjd_err::QJD_NOT_FOUND) + Err(qjson_err::QJSON_NOT_FOUND) } /// Given the indices position of a value's first marker, return: @@ -175,9 +175,9 @@ fn resolve_in_known_children( /// - container: index after the matching closer (= closer_idx + 1) /// - string: index after the close '"' (= start + 2) /// - scalar: start itself (indices[start] IS the separator/closer) -pub(crate) fn find_value_span(doc: &Document, start: u32) -> Result<(u32, u32), qjd_err> { +pub(crate) fn find_value_span(doc: &Document, start: u32) -> Result<(u32, u32), qjson_err> { let pos = doc.indices[start as usize] as usize; - let b = *doc.buf.get(pos).ok_or(qjd_err::QJD_PARSE_ERROR)?; + let b = *doc.buf.get(pos).ok_or(qjson_err::QJSON_PARSE_ERROR)?; match b { b'{' | b'[' => { // Brace-count to matching closer. @@ -186,14 +186,14 @@ pub(crate) fn find_value_span(doc: &Document, start: u32) -> Result<(u32, u32), let mut k = start + 1; while (k as usize) < doc.indices.len() { let cb_pos = doc.indices[k as usize] as usize; - if cb_pos >= doc.buf.len() { return Err(qjd_err::QJD_PARSE_ERROR); } + if cb_pos >= doc.buf.len() { return Err(qjson_err::QJSON_PARSE_ERROR); } let cb = doc.buf[cb_pos]; match cb { b'{' | b'[' => depth += 1, b'}' | b']' => { depth -= 1; if depth == 0 { - if cb != want_close { return Err(qjd_err::QJD_PARSE_ERROR); } + if cb != want_close { return Err(qjson_err::QJSON_PARSE_ERROR); } // cursor_end = closer index (k) // skip_end = one past closer (k+1), pointing at ',' // or parent closer @@ -204,7 +204,7 @@ pub(crate) fn find_value_span(doc: &Document, start: u32) -> Result<(u32, u32), } k += 1; } - Err(qjd_err::QJD_PARSE_ERROR) + Err(qjson_err::QJSON_PARSE_ERROR) } b'"' => { // String value: indices has both opening (start) and closing (start+1) quotes. @@ -222,11 +222,11 @@ pub(crate) fn find_value_span(doc: &Document, start: u32) -> Result<(u32, u32), } } -pub(crate) fn resolve_single_key(doc: &Document, cur: Cursor, key: &[u8]) -> Result { +pub(crate) fn resolve_single_key(doc: &Document, cur: Cursor, key: &[u8]) -> Result { step(doc, cur, &PathSeg::Key(key)) } -pub(crate) fn resolve_single_idx(doc: &Document, cur: Cursor, idx: u32) -> Result { +pub(crate) fn resolve_single_idx(doc: &Document, cur: Cursor, idx: u32) -> Result { step(doc, cur, &PathSeg::Idx(idx)) } @@ -260,21 +260,21 @@ mod tests { fn missing_key_is_not_found() { let d = doc_of(b"{\"a\":1}"); let r = Cursor::root(&d).resolve(&d, b"b"); - assert_eq!(r, Err(qjd_err::QJD_NOT_FOUND)); + assert_eq!(r, Err(qjson_err::QJSON_NOT_FOUND)); } #[test] fn type_mismatch_on_index_into_object() { let d = doc_of(b"{\"a\":1}"); let r = Cursor::root(&d).resolve(&d, b"[0]"); - assert_eq!(r, Err(qjd_err::QJD_TYPE_MISMATCH)); + assert_eq!(r, Err(qjson_err::QJSON_TYPE_MISMATCH)); } #[test] fn type_mismatch_on_key_into_array() { let d = doc_of(b"[1,2,3]"); let r = Cursor::root(&d).resolve(&d, b"a"); - assert_eq!(r, Err(qjd_err::QJD_TYPE_MISMATCH)); + assert_eq!(r, Err(qjson_err::QJSON_TYPE_MISMATCH)); } #[test] @@ -287,7 +287,7 @@ mod tests { fn array_out_of_bounds() { let d = doc_of(b"[10,20]"); let r = Cursor::root(&d).resolve(&d, b"[5]"); - assert_eq!(r, Err(qjd_err::QJD_NOT_FOUND)); + assert_eq!(r, Err(qjson_err::QJSON_NOT_FOUND)); } #[test] diff --git a/src/decode/number.rs b/src/decode/number.rs index 1beda2d..d24ebfb 100644 --- a/src/decode/number.rs +++ b/src/decode/number.rs @@ -1,10 +1,10 @@ -use crate::error::qjd_err; +use crate::error::qjson_err; -pub(crate) fn parse_i64(bytes: &[u8]) -> Result { +pub(crate) fn parse_i64(bytes: &[u8]) -> Result { crate::validate::validate_number(bytes)?; // After ABNF validation, integer-only inputs have no `.`/`e`/`E`. if bytes.iter().any(|&b| b == b'.' || b == b'e' || b == b'E') { - return Err(qjd_err::QJD_TYPE_MISMATCH); + return Err(qjson_err::QJSON_TYPE_MISMATCH); } let (neg, rest) = match bytes[0] { b'-' => (true, &bytes[1..]), @@ -18,19 +18,19 @@ pub(crate) fn parse_i64(bytes: &[u8]) -> Result { if neg { x.checked_sub(d) } else { x.checked_add(d) } }) { Some(n) => n, - None => return Err(qjd_err::QJD_OUT_OF_RANGE), + None => return Err(qjson_err::QJSON_OUT_OF_RANGE), }; } Ok(v) } -pub(crate) fn parse_f64(bytes: &[u8]) -> Result { +pub(crate) fn parse_f64(bytes: &[u8]) -> Result { crate::validate::validate_number(bytes)?; - let s = std::str::from_utf8(bytes).map_err(|_| qjd_err::QJD_DECODE_FAILED)?; + let s = std::str::from_utf8(bytes).map_err(|_| qjson_err::QJSON_DECODE_FAILED)?; match s.parse::() { Ok(v) if v.is_finite() => Ok(v), - Ok(_) => Err(qjd_err::QJD_NUMBER_OUT_OF_RANGE), - Err(_) => Err(qjd_err::QJD_DECODE_FAILED), + Ok(_) => Err(qjson_err::QJSON_NUMBER_OUT_OF_RANGE), + Err(_) => Err(qjson_err::QJSON_DECODE_FAILED), } } @@ -46,22 +46,22 @@ mod tests { #[test] fn i64_overflow() { - assert_eq!(parse_i64(b"9223372036854775808"), Err(qjd_err::QJD_OUT_OF_RANGE)); + assert_eq!(parse_i64(b"9223372036854775808"), Err(qjson_err::QJSON_OUT_OF_RANGE)); } #[test] fn i64_rejects_decimal() { - assert_eq!(parse_i64(b"1.5"), Err(qjd_err::QJD_TYPE_MISMATCH)); + assert_eq!(parse_i64(b"1.5"), Err(qjson_err::QJSON_TYPE_MISMATCH)); } #[test] fn i64_rejects_exponent() { - assert_eq!(parse_i64(b"1e5"), Err(qjd_err::QJD_TYPE_MISMATCH)); + assert_eq!(parse_i64(b"1e5"), Err(qjson_err::QJSON_TYPE_MISMATCH)); } #[test] fn i64_rejects_empty() { - assert_eq!(parse_i64(b""), Err(qjd_err::QJD_INVALID_NUMBER)); + assert_eq!(parse_i64(b""), Err(qjson_err::QJSON_INVALID_NUMBER)); } #[test] fn f64_zero() { assert_eq!(parse_f64(b"0.0").unwrap(), 0.0); } @@ -71,6 +71,6 @@ mod tests { #[test] fn f64_rejects_garbage() { - assert_eq!(parse_f64(b"hello"), Err(qjd_err::QJD_INVALID_NUMBER)); + assert_eq!(parse_f64(b"hello"), Err(qjson_err::QJSON_INVALID_NUMBER)); } } diff --git a/src/decode/string.rs b/src/decode/string.rs index 8572441..7e1ac15 100644 --- a/src/decode/string.rs +++ b/src/decode/string.rs @@ -1,11 +1,11 @@ -use crate::error::qjd_err; +use crate::error::qjson_err; /// Decode the JSON string between `start` and `end` (exclusive of the /// surrounding quotes) into `scratch` if escapes are present. Returns /// (ptr, len) pointing into either `buf` (no escapes) or `scratch`. pub(crate) fn decode_string( buf: &[u8], start: usize, end: usize, scratch: &mut Vec, -) -> Result<(*const u8, usize), qjd_err> { +) -> Result<(*const u8, usize), qjson_err> { let slice = &buf[start..end]; crate::validate::validate_string_span(slice)?; if memchr::memchr(b'\\', slice).is_none() { @@ -24,7 +24,7 @@ pub(crate) fn decode_string( continue; } // Escape. - if i + 1 >= slice.len() { return Err(qjd_err::QJD_DECODE_FAILED); } + if i + 1 >= slice.len() { return Err(qjson_err::QJSON_DECODE_FAILED); } match slice[i + 1] { b'"' => { scratch.push(b'"'); i += 2; } b'\\' => { scratch.push(b'\\'); i += 2; } @@ -35,35 +35,35 @@ pub(crate) fn decode_string( b'r' => { scratch.push(b'\r'); i += 2; } b't' => { scratch.push(b'\t'); i += 2; } b'u' => { - if i + 6 > slice.len() { return Err(qjd_err::QJD_DECODE_FAILED); } + if i + 6 > slice.len() { return Err(qjson_err::QJSON_DECODE_FAILED); } let h = parse_hex4(&slice[i + 2 .. i + 6])?; i += 6; let cp = if (0xD800..=0xDBFF).contains(&h) { // High surrogate; expect low surrogate next. if i + 6 > slice.len() || &slice[i..i + 2] != b"\\u" { - return Err(qjd_err::QJD_DECODE_FAILED); + return Err(qjson_err::QJSON_DECODE_FAILED); } let l = parse_hex4(&slice[i + 2 .. i + 6])?; if !(0xDC00..=0xDFFF).contains(&l) { - return Err(qjd_err::QJD_DECODE_FAILED); + return Err(qjson_err::QJSON_DECODE_FAILED); } i += 6; 0x10000 + ((h - 0xD800) << 10) + (l - 0xDC00) } else if (0xDC00..=0xDFFF).contains(&h) { - return Err(qjd_err::QJD_DECODE_FAILED); + return Err(qjson_err::QJSON_DECODE_FAILED); } else { h }; encode_utf8(cp, scratch); } - _ => return Err(qjd_err::QJD_DECODE_FAILED), + _ => return Err(qjson_err::QJSON_DECODE_FAILED), } } Ok((scratch.as_ptr(), scratch.len())) } -fn parse_hex4(bytes: &[u8]) -> Result { +fn parse_hex4(bytes: &[u8]) -> Result { let mut v: u32 = 0; for &b in bytes { v <<= 4; @@ -71,7 +71,7 @@ fn parse_hex4(bytes: &[u8]) -> Result { b'0'..=b'9' => (b - b'0') as u32, b'a'..=b'f' => (b - b'a' + 10) as u32, b'A'..=b'F' => (b - b'A' + 10) as u32, - _ => return Err(qjd_err::QJD_DECODE_FAILED), + _ => return Err(qjson_err::QJSON_DECODE_FAILED), }; } Ok(v) @@ -99,7 +99,7 @@ fn encode_utf8(cp: u32, out: &mut Vec) { mod tests { use super::*; - fn d(s: &[u8]) -> Result, qjd_err> { + fn d(s: &[u8]) -> Result, qjson_err> { let mut scratch = Vec::new(); let (p, n) = decode_string(s, 0, s.len(), &mut scratch)?; Ok(unsafe { std::slice::from_raw_parts(p, n) }.to_vec()) @@ -159,26 +159,26 @@ mod tests { #[test] fn lone_high_surrogate_fails() { - assert_eq!(d(b"\\uD83D").unwrap_err(), qjd_err::QJD_DECODE_FAILED); + assert_eq!(d(b"\\uD83D").unwrap_err(), qjson_err::QJSON_DECODE_FAILED); } #[test] fn invalid_hex_in_unicode_fails() { // validate_string_span (called first) catches non-hex digits as - // QJD_INVALID_STRING; the decode loop would also catch it as - // QJD_DECODE_FAILED, but we never reach it. - assert_eq!(d(b"\\uZZZZ").unwrap_err(), qjd_err::QJD_INVALID_STRING); + // QJSON_INVALID_STRING; the decode loop would also catch it as + // QJSON_DECODE_FAILED, but we never reach it. + assert_eq!(d(b"\\uZZZZ").unwrap_err(), qjson_err::QJSON_INVALID_STRING); } #[test] fn unknown_escape_fails() { // validate_string_span catches unknown escape introducers first. - assert_eq!(d(b"\\q").unwrap_err(), qjd_err::QJD_INVALID_STRING); + assert_eq!(d(b"\\q").unwrap_err(), qjson_err::QJSON_INVALID_STRING); } #[test] fn dangling_backslash_fails() { // validate_string_span catches a trailing lone backslash first. - assert_eq!(d(b"a\\").unwrap_err(), qjd_err::QJD_INVALID_STRING); + assert_eq!(d(b"a\\").unwrap_err(), qjson_err::QJSON_INVALID_STRING); } } diff --git a/src/doc.rs b/src/doc.rs index d20e17f..326e6d9 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -1,6 +1,6 @@ use std::cell::RefCell; -use crate::error::qjd_err; +use crate::error::qjson_err; use crate::skip_cache::SkipCache; pub struct Document<'a> { @@ -11,23 +11,23 @@ pub struct Document<'a> { } impl<'a> Document<'a> { - pub fn parse(buf: &'a [u8]) -> Result { + pub fn parse(buf: &'a [u8]) -> Result { Self::parse_with_options(buf, &crate::options::Options::default()) } pub fn parse_with_options( buf: &'a [u8], opts: &crate::options::Options, - ) -> Result { + ) -> Result { // RFC 8259 §2: "A JSON text is a serialized value." // Empty input and whitespace-only input contain no value. if buf.iter().all(|&b| matches!(b, b' ' | b'\t' | b'\n' | b'\r')) { - return Err(qjd_err::QJD_PARSE_ERROR); + return Err(qjson_err::QJSON_PARSE_ERROR); } let max_depth = opts.effective_max_depth(); let mut indices = Vec::new(); - crate::scan::scan(buf, &mut indices).map_err(|_| qjd_err::QJD_PARSE_ERROR)?; + crate::scan::scan(buf, &mut indices).map_err(|_| qjson_err::QJSON_PARSE_ERROR)?; indices.push(u32::MAX); crate::validate::validate_depth(buf, &indices, max_depth)?; @@ -47,28 +47,28 @@ impl<'a> Document<'a> { } use crate::cursor::{Cursor, find_value_span}; -use crate::error::qjd_type; +use crate::error::qjson_type; impl<'a> Document<'a> { /// Inspect a cursor and return its JSON value type. - pub(crate) fn type_of(&self, cur: Cursor) -> Result { + pub(crate) fn type_of(&self, cur: Cursor) -> Result { let pos = *self.indices.get(cur.idx_start as usize) - .ok_or(qjd_err::QJD_PARSE_ERROR)? as usize; - let lead = self.buf.get(pos).copied().ok_or(qjd_err::QJD_PARSE_ERROR)?; + .ok_or(qjson_err::QJSON_PARSE_ERROR)? as usize; + let lead = self.buf.get(pos).copied().ok_or(qjson_err::QJSON_PARSE_ERROR)?; match lead { - b'"' => Ok(qjd_type::QJD_T_STR), - b'{' => Ok(qjd_type::QJD_T_OBJ), - b'[' => Ok(qjd_type::QJD_T_ARR), + b'"' => Ok(qjson_type::QJSON_T_STR), + b'{' => Ok(qjson_type::QJSON_T_OBJ), + b'[' => Ok(qjson_type::QJSON_T_ARR), _ => { // For a scalar value the cursor's idx_start points at the // structural char AFTER the scalar; the scalar's first byte // lives between the previous structural char and this one. let scalar_start = self.find_scalar_start(cur.idx_start)?; match self.buf.get(scalar_start).copied() { - Some(b't') | Some(b'f') => Ok(qjd_type::QJD_T_BOOL), - Some(b'n') => Ok(qjd_type::QJD_T_NULL), - Some(b'-') | Some(b'0'..=b'9') => Ok(qjd_type::QJD_T_NUM), - _ => Err(qjd_err::QJD_PARSE_ERROR), + Some(b't') | Some(b'f') => Ok(qjson_type::QJSON_T_BOOL), + Some(b'n') => Ok(qjson_type::QJSON_T_NULL), + Some(b'-') | Some(b'0'..=b'9') => Ok(qjson_type::QJSON_T_NUM), + _ => Err(qjson_err::QJSON_PARSE_ERROR), } } } @@ -77,8 +77,8 @@ impl<'a> Document<'a> { /// Find the byte position of the first non-whitespace byte after the /// structural character at `indices[idx - 1]`. Used to locate the first /// byte of a scalar value. - pub(crate) fn find_scalar_start(&self, idx: u32) -> Result { - if idx == 0 { return Err(qjd_err::QJD_PARSE_ERROR); } + pub(crate) fn find_scalar_start(&self, idx: u32) -> Result { + if idx == 0 { return Err(qjson_err::QJSON_PARSE_ERROR); } let prev = self.indices[(idx - 1) as usize] as usize; let mut p = prev + 1; while p < self.buf.len() && matches!(self.buf[p], b' '|b'\t'|b'\n'|b'\r') { @@ -91,13 +91,13 @@ impl<'a> Document<'a> { /// indices position of the key (so the caller can decode it via the /// existing string-decode path) and the value's `Cursor`. /// - /// Returns `QJD_TYPE_MISMATCH` for non-object cursors, `QJD_NOT_FOUND` + /// Returns `QJSON_TYPE_MISMATCH` for non-object cursors, `QJSON_NOT_FOUND` /// when `i` is past the end. - pub(crate) fn nth_object_entry(&self, cur: Cursor, n: usize) -> Result<(u32, Cursor), qjd_err> { + pub(crate) fn nth_object_entry(&self, cur: Cursor, n: usize) -> Result<(u32, Cursor), qjson_err> { let pos = self.indices[cur.idx_start as usize] as usize; - let b = *self.buf.get(pos).ok_or(qjd_err::QJD_PARSE_ERROR)?; + let b = *self.buf.get(pos).ok_or(qjson_err::QJSON_PARSE_ERROR)?; if b != b'{' { - return Err(qjd_err::QJD_TYPE_MISMATCH); + return Err(qjson_err::QJSON_TYPE_MISMATCH); } // Mirror cursor_len's walk, but stop at the n-th child rather than counting. let closer_pos = self.indices[cur.idx_end as usize] as usize; @@ -106,7 +106,7 @@ impl<'a> Document<'a> { p += 1; } if p == closer_pos { - return Err(qjd_err::QJD_NOT_FOUND); + return Err(qjson_err::QJSON_NOT_FOUND); } let mut i = cur.idx_start + 1; let end = cur.idx_end; @@ -122,25 +122,25 @@ impl<'a> Document<'a> { } count += 1; let after_pos = self.indices[skip_end as usize] as usize; - if after_pos >= self.buf.len() { return Err(qjd_err::QJD_PARSE_ERROR); } + if after_pos >= self.buf.len() { return Err(qjson_err::QJSON_PARSE_ERROR); } match self.buf[after_pos] { b',' => { i = skip_end + 1; - if i > end { return Err(qjd_err::QJD_NOT_FOUND); } + if i > end { return Err(qjson_err::QJSON_NOT_FOUND); } } - b'}' => return Err(qjd_err::QJD_NOT_FOUND), - _ => return Err(qjd_err::QJD_PARSE_ERROR), + b'}' => return Err(qjson_err::QJSON_NOT_FOUND), + _ => return Err(qjson_err::QJSON_PARSE_ERROR), } } } /// Count direct children of the container at `cur`. - /// Returns QJD_TYPE_MISMATCH for non-container cursors. - pub(crate) fn cursor_len(&self, cur: Cursor) -> Result { + /// Returns QJSON_TYPE_MISMATCH for non-container cursors. + pub(crate) fn cursor_len(&self, cur: Cursor) -> Result { let pos = self.indices[cur.idx_start as usize] as usize; - let b = *self.buf.get(pos).ok_or(qjd_err::QJD_PARSE_ERROR)?; + let b = *self.buf.get(pos).ok_or(qjson_err::QJSON_PARSE_ERROR)?; if b != b'{' && b != b'[' { - return Err(qjd_err::QJD_TYPE_MISMATCH); + return Err(qjson_err::QJSON_TYPE_MISMATCH); } let is_obj = b == b'{'; // Empty container detection: byte after opener (skipping whitespace) @@ -161,14 +161,14 @@ impl<'a> Document<'a> { let value_idx_start = if is_obj { i + 3 } else { i }; let (_cursor_end, skip_end) = find_value_span(self, value_idx_start)?; let after_pos = self.indices[skip_end as usize] as usize; - if after_pos >= self.buf.len() { return Err(qjd_err::QJD_PARSE_ERROR); } + if after_pos >= self.buf.len() { return Err(qjson_err::QJSON_PARSE_ERROR); } match self.buf[after_pos] { b',' => { i = skip_end + 1; - if i > end { return Err(qjd_err::QJD_PARSE_ERROR); } + if i > end { return Err(qjson_err::QJSON_PARSE_ERROR); } } b'}' | b']' => break, - _ => return Err(qjd_err::QJD_PARSE_ERROR), + _ => return Err(qjson_err::QJSON_PARSE_ERROR), } } Ok(count) @@ -202,7 +202,7 @@ mod tests { #[test] fn parse_with_lazy_skips_eager_validation() { // Trailing content is an eager-only check; lazy must accept it. - let opts = crate::options::Options { mode: crate::options::QJD_MODE_LAZY, max_depth: 0 }; + let opts = crate::options::Options { mode: crate::options::QJSON_MODE_LAZY, max_depth: 0 }; assert!(Document::parse_with_options(b"{}garbage", &opts).is_ok()); } } diff --git a/src/error.rs b/src/error.rs index 72ff3e9..4a6f60f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,52 +2,52 @@ #[repr(C)] #[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum qjd_err { - QJD_OK = 0, - QJD_PARSE_ERROR = 1, - QJD_NOT_FOUND = 2, - QJD_TYPE_MISMATCH = 3, - QJD_OUT_OF_RANGE = 4, - QJD_DECODE_FAILED = 5, - QJD_INVALID_PATH = 6, - QJD_INVALID_ARG = 7, - QJD_OOM = 8, - QJD_NESTING_TOO_DEEP = 9, - QJD_TRAILING_CONTENT = 10, - QJD_NUMBER_OUT_OF_RANGE = 11, - QJD_INVALID_NUMBER = 12, - QJD_INVALID_STRING = 13, - QJD_INVALID_UTF8 = 14, +pub enum qjson_err { + QJSON_OK = 0, + QJSON_PARSE_ERROR = 1, + QJSON_NOT_FOUND = 2, + QJSON_TYPE_MISMATCH = 3, + QJSON_OUT_OF_RANGE = 4, + QJSON_DECODE_FAILED = 5, + QJSON_INVALID_PATH = 6, + QJSON_INVALID_ARG = 7, + QJSON_OOM = 8, + QJSON_NESTING_TOO_DEEP = 9, + QJSON_TRAILING_CONTENT = 10, + QJSON_NUMBER_OUT_OF_RANGE = 11, + QJSON_INVALID_NUMBER = 12, + QJSON_INVALID_STRING = 13, + QJSON_INVALID_UTF8 = 14, } #[repr(C)] #[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum qjd_type { - QJD_T_NULL = 0, - QJD_T_BOOL = 1, - QJD_T_NUM = 2, - QJD_T_STR = 3, - QJD_T_ARR = 4, - QJD_T_OBJ = 5, +pub enum qjson_type { + QJSON_T_NULL = 0, + QJSON_T_BOOL = 1, + QJSON_T_NUM = 2, + QJSON_T_STR = 3, + QJSON_T_ARR = 4, + QJSON_T_OBJ = 5, } -pub fn strerror(code: qjd_err) -> &'static str { +pub fn strerror(code: qjson_err) -> &'static str { match code { - qjd_err::QJD_OK => "ok", - qjd_err::QJD_PARSE_ERROR => "JSON parse error", - qjd_err::QJD_NOT_FOUND => "path not found", - qjd_err::QJD_TYPE_MISMATCH => "type mismatch at path", - qjd_err::QJD_OUT_OF_RANGE => "numeric out of range", - qjd_err::QJD_DECODE_FAILED => "decode failed", - qjd_err::QJD_INVALID_PATH => "invalid path syntax", - qjd_err::QJD_INVALID_ARG => "invalid argument", - qjd_err::QJD_OOM => "out of memory", - qjd_err::QJD_NESTING_TOO_DEEP => "nesting depth exceeds limit", - qjd_err::QJD_TRAILING_CONTENT => "trailing content after root value", - qjd_err::QJD_NUMBER_OUT_OF_RANGE => "number out of representable range", - qjd_err::QJD_INVALID_NUMBER => "invalid number format (RFC 8259)", - qjd_err::QJD_INVALID_STRING => "invalid string content (unescaped control char)", - qjd_err::QJD_INVALID_UTF8 => "invalid UTF-8 in string", + qjson_err::QJSON_OK => "ok", + qjson_err::QJSON_PARSE_ERROR => "JSON parse error", + qjson_err::QJSON_NOT_FOUND => "path not found", + qjson_err::QJSON_TYPE_MISMATCH => "type mismatch at path", + qjson_err::QJSON_OUT_OF_RANGE => "numeric out of range", + qjson_err::QJSON_DECODE_FAILED => "decode failed", + qjson_err::QJSON_INVALID_PATH => "invalid path syntax", + qjson_err::QJSON_INVALID_ARG => "invalid argument", + qjson_err::QJSON_OOM => "out of memory", + qjson_err::QJSON_NESTING_TOO_DEEP => "nesting depth exceeds limit", + qjson_err::QJSON_TRAILING_CONTENT => "trailing content after root value", + qjson_err::QJSON_NUMBER_OUT_OF_RANGE => "number out of representable range", + qjson_err::QJSON_INVALID_NUMBER => "invalid number format (RFC 8259)", + qjson_err::QJSON_INVALID_STRING => "invalid string content (unescaped control char)", + qjson_err::QJSON_INVALID_UTF8 => "invalid UTF-8 in string", } } @@ -58,13 +58,13 @@ mod tests { #[test] fn strerror_covers_every_variant() { for code in [ - qjd_err::QJD_OK, qjd_err::QJD_PARSE_ERROR, qjd_err::QJD_NOT_FOUND, - qjd_err::QJD_TYPE_MISMATCH, qjd_err::QJD_OUT_OF_RANGE, - qjd_err::QJD_DECODE_FAILED, qjd_err::QJD_INVALID_PATH, - qjd_err::QJD_INVALID_ARG, qjd_err::QJD_OOM, - qjd_err::QJD_NESTING_TOO_DEEP, qjd_err::QJD_TRAILING_CONTENT, - qjd_err::QJD_NUMBER_OUT_OF_RANGE, qjd_err::QJD_INVALID_NUMBER, - qjd_err::QJD_INVALID_STRING, qjd_err::QJD_INVALID_UTF8, + qjson_err::QJSON_OK, qjson_err::QJSON_PARSE_ERROR, qjson_err::QJSON_NOT_FOUND, + qjson_err::QJSON_TYPE_MISMATCH, qjson_err::QJSON_OUT_OF_RANGE, + qjson_err::QJSON_DECODE_FAILED, qjson_err::QJSON_INVALID_PATH, + qjson_err::QJSON_INVALID_ARG, qjson_err::QJSON_OOM, + qjson_err::QJSON_NESTING_TOO_DEEP, qjson_err::QJSON_TRAILING_CONTENT, + qjson_err::QJSON_NUMBER_OUT_OF_RANGE, qjson_err::QJSON_INVALID_NUMBER, + qjson_err::QJSON_INVALID_STRING, qjson_err::QJSON_INVALID_UTF8, ] { assert!(!strerror(code).is_empty()); } diff --git a/src/ffi.rs b/src/ffi.rs index 4cecaef..d4d8cec 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -1,27 +1,27 @@ //! C ABI surface. Every public function is `unsafe extern "C"`. -//! All public symbols use the `qjd_` prefix. +//! All public symbols use the `qjson_` prefix. //! //! # Shared safety contract //! //! Most exports share the same FFI obligations on the caller: //! -//! - A `*mut qjd_doc` argument must be NULL or a pointer previously returned -//! by [`qjd_parse`] that has not yet been passed to [`qjd_free`]. -//! - The input buffer passed to [`qjd_parse`] must remain valid and +//! - A `*mut qjson_doc` argument must be NULL or a pointer previously returned +//! by [`qjson_parse`] that has not yet been passed to [`qjson_free`]. +//! - The input buffer passed to [`qjson_parse`] must remain valid and //! unmodified for as long as the document is alive; the document borrows it. //! - Path / key pointer arguments must point to the indicated number of //! readable bytes, or be NULL when the length is `0`. //! - Out pointers must be non-NULL and writable for their target type when -//! the function returns `QJD_OK`. Functions return `QJD_INVALID_ARG` +//! the function returns `QJSON_OK`. Functions return `QJSON_INVALID_ARG` //! instead of writing through a NULL out pointer. -//! - A `*const qjd_cursor` must point to a cursor produced by one of -//! [`qjd_open`], [`qjd_cursor_open`], [`qjd_cursor_field`], or -//! [`qjd_cursor_index`], and whose `doc` field is still alive. +//! - A `*const qjson_cursor` must point to a cursor produced by one of +//! [`qjson_open`], [`qjson_cursor_open`], [`qjson_cursor_field`], or +//! [`qjson_cursor_index`], and whose `doc` field is still alive. //! - A pointer/length pair returned by any `*_get_str` is invalidated by //! the next `*_get_str` call on the same document (scratch buffer reuse). //! //! Every export catches Rust panics at the FFI boundary and converts them -//! to `QJD_OOM`; a panic must not be allowed to unwind across the boundary. +//! to `QJSON_OOM`; a panic must not be allowed to unwind across the boundary. #![allow(non_camel_case_types)] @@ -29,20 +29,20 @@ use std::os::raw::{c_char, c_int}; use std::ptr; use crate::doc::Document; -use crate::error::qjd_err; +use crate::error::qjson_err; macro_rules! ffi_catch { ($body:block) => {{ let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| $body)); match r { Ok(code) => code, - Err(_) => qjd_err::QJD_OOM as c_int, + Err(_) => qjson_err::QJSON_OOM as c_int, } }}; } -/// Opaque type exported to C as `qjd_doc*`. -pub struct qjd_doc(pub(crate) Document<'static>); +/// Opaque type exported to C as `qjson_doc*`. +pub struct qjson_doc(pub(crate) Document<'static>); /// Return a static NUL-terminated message for the given error code. /// @@ -52,7 +52,7 @@ pub struct qjd_doc(pub(crate) Document<'static>); /// with the rest of the surface. The returned pointer is to static storage /// and must not be freed. #[no_mangle] -pub unsafe extern "C" fn qjd_strerror(code: c_int) -> *const c_char { +pub unsafe extern "C" fn qjson_strerror(code: c_int) -> *const c_char { // Hardcoded NUL-terminated map; avoids runtime allocation and lifetime issues. let s: &'static [u8] = match code { 0 => b"ok\0", @@ -80,21 +80,21 @@ pub unsafe extern "C" fn qjd_strerror(code: c_int) -> *const c_char { /// # Safety /// /// - `buf` must point to `len` readable bytes, or be NULL (in which case the -/// function returns NULL with `*err_out = QJD_INVALID_ARG`). -/// - `err_out` may be NULL. When non-NULL it receives `QJD_OK` on success or +/// function returns NULL with `*err_out = QJSON_INVALID_ARG`). +/// - `err_out` may be NULL. When non-NULL it receives `QJSON_OK` on success or /// an error code on failure. /// - The buffer must remain valid and unmodified for the lifetime of the -/// returned `qjd_doc*`; the document borrows it. +/// returned `qjson_doc*`; the document borrows it. /// - On success, the returned pointer must be freed exactly once with -/// [`qjd_free`]. +/// [`qjson_free`]. #[no_mangle] -pub unsafe extern "C" fn qjd_parse( +pub unsafe extern "C" fn qjson_parse( buf: *const u8, len: usize, err_out: *mut c_int, -) -> *mut qjd_doc { +) -> *mut qjson_doc { let default = crate::options::Options::default(); - qjd_parse_ex(buf, len, &default as *const _, err_out) + qjson_parse_ex(buf, len, &default as *const _, err_out) } /// Parse with caller-supplied options. `opts` may be NULL to mean defaults @@ -102,19 +102,19 @@ pub unsafe extern "C" fn qjd_parse( /// /// # Safety /// -/// Same as `qjd_parse`, with the additional contract that `opts`, when -/// non-NULL, points to a readable `qjd_options` for the duration of the call +/// Same as `qjson_parse`, with the additional contract that `opts`, when +/// non-NULL, points to a readable `qjson_options` for the duration of the call /// (the struct is copied internally). #[no_mangle] -pub unsafe extern "C" fn qjd_parse_ex( +pub unsafe extern "C" fn qjson_parse_ex( buf: *const u8, len: usize, opts: *const crate::options::Options, err_out: *mut c_int, -) -> *mut qjd_doc { +) -> *mut qjson_doc { let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { if buf.is_null() { - if !err_out.is_null() { *err_out = qjd_err::QJD_INVALID_ARG as c_int; } + if !err_out.is_null() { *err_out = qjson_err::QJSON_INVALID_ARG as c_int; } return ptr::null_mut(); } let opts_owned = if opts.is_null() { @@ -125,8 +125,8 @@ pub unsafe extern "C" fn qjd_parse_ex( let slice: &'static [u8] = std::slice::from_raw_parts(buf, len); match Document::parse_with_options(slice, &opts_owned) { Ok(d) => { - if !err_out.is_null() { *err_out = qjd_err::QJD_OK as c_int; } - Box::into_raw(Box::new(qjd_doc(d))) + if !err_out.is_null() { *err_out = qjson_err::QJSON_OK as c_int; } + Box::into_raw(Box::new(qjson_doc(d))) } Err(e) => { if !err_out.is_null() { *err_out = e as c_int; } @@ -137,33 +137,35 @@ pub unsafe extern "C" fn qjd_parse_ex( match r { Ok(p) => p, Err(_) => { - if !err_out.is_null() { *err_out = qjd_err::QJD_OOM as c_int; } + if !err_out.is_null() { *err_out = qjson_err::QJSON_OOM as c_int; } std::ptr::null_mut() } } } -/// Free a document returned by [`qjd_parse`]. NULL is a no-op. +/// Free a document returned by [`qjson_parse`]. NULL is a no-op. /// /// # Safety /// -/// `doc` must be NULL or a pointer previously returned by [`qjd_parse`] +/// `doc` must be NULL or a pointer previously returned by [`qjson_parse`] /// that has not yet been freed. Double-free or passing a pointer not -/// produced by `qjd_parse` is undefined behavior. +/// produced by `qjson_parse` is undefined behavior. #[no_mangle] -pub unsafe extern "C" fn qjd_free(doc: *mut qjd_doc) { - if doc.is_null() { return; } - let _ = Box::from_raw(doc); +pub unsafe extern "C" fn qjson_free(doc: *mut qjson_doc) { + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + if doc.is_null() { return; } + let _ = Box::from_raw(doc); + })); } use crate::cursor::Cursor; -use crate::error::qjd_type; +use crate::error::qjson_type; unsafe fn resolve_root_path( - doc: *mut qjd_doc, path: *const c_char, path_len: usize, -) -> Result<(&'static Document<'static>, Cursor), qjd_err> { + doc: *mut qjson_doc, path: *const c_char, path_len: usize, +) -> Result<(&'static Document<'static>, Cursor), qjson_err> { if doc.is_null() || (path.is_null() && path_len != 0) { - return Err(qjd_err::QJD_INVALID_ARG); + return Err(qjson_err::QJSON_INVALID_ARG); } let d: &Document = &(*doc).0; let p: &[u8] = if path.is_null() { @@ -175,7 +177,7 @@ unsafe fn resolve_root_path( Ok((std::mem::transmute::<&Document<'_>, &'static Document<'static>>(d), cur)) } -/// Write the JSON value type at `path` into `*type_out` (see [`qjd_type`]). +/// Write the JSON value type at `path` into `*type_out` (see [`qjson_type`]). /// /// # Safety /// @@ -183,14 +185,14 @@ unsafe fn resolve_root_path( /// `doc` must be live or NULL; `path` must point to `path_len` bytes or be /// NULL with `path_len == 0`; `type_out` must be non-NULL and writable. #[no_mangle] -pub unsafe extern "C" fn qjd_typeof( - doc: *mut qjd_doc, path: *const c_char, path_len: usize, type_out: *mut c_int, +pub unsafe extern "C" fn qjson_typeof( + doc: *mut qjson_doc, path: *const c_char, path_len: usize, type_out: *mut c_int, ) -> c_int { ffi_catch!({ - if type_out.is_null() { return qjd_err::QJD_INVALID_ARG as c_int; } + if type_out.is_null() { return qjson_err::QJSON_INVALID_ARG as c_int; } match resolve_root_path(doc, path, path_len) { Ok((d, cur)) => match d.type_of(cur) { - Ok(t) => { *type_out = t as c_int; qjd_err::QJD_OK as c_int } + Ok(t) => { *type_out = t as c_int; qjson_err::QJSON_OK as c_int } Err(e) => e as c_int, }, Err(e) => e as c_int, @@ -206,15 +208,15 @@ pub unsafe extern "C" fn qjd_typeof( /// `doc` must be live or NULL; `path` must point to `path_len` bytes or be /// NULL with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] -pub unsafe extern "C" fn qjd_is_null( - doc: *mut qjd_doc, path: *const c_char, path_len: usize, out: *mut c_int, +pub unsafe extern "C" fn qjson_is_null( + doc: *mut qjson_doc, path: *const c_char, path_len: usize, out: *mut c_int, ) -> c_int { ffi_catch!({ - if out.is_null() { return qjd_err::QJD_INVALID_ARG as c_int; } + if out.is_null() { return qjson_err::QJSON_INVALID_ARG as c_int; } match resolve_root_path(doc, path, path_len) { Ok((d, cur)) => match d.type_of(cur) { - Ok(qjd_type::QJD_T_NULL) => { *out = 1; qjd_err::QJD_OK as c_int } - Ok(_) => { *out = 0; qjd_err::QJD_OK as c_int } + Ok(qjson_type::QJSON_T_NULL) => { *out = 1; qjson_err::QJSON_OK as c_int } + Ok(_) => { *out = 0; qjson_err::QJSON_OK as c_int } Err(e) => e as c_int, }, Err(e) => e as c_int, @@ -223,7 +225,7 @@ pub unsafe extern "C" fn qjd_is_null( } /// Write the number of direct children of the container at `path` into `*out`. -/// Returns `QJD_TYPE_MISMATCH` if the target is not an array or object. +/// Returns `QJSON_TYPE_MISMATCH` if the target is not an array or object. /// /// # Safety /// @@ -231,14 +233,14 @@ pub unsafe extern "C" fn qjd_is_null( /// `doc` must be live or NULL; `path` must point to `path_len` bytes or be /// NULL with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] -pub unsafe extern "C" fn qjd_len( - doc: *mut qjd_doc, path: *const c_char, path_len: usize, out: *mut usize, +pub unsafe extern "C" fn qjson_len( + doc: *mut qjson_doc, path: *const c_char, path_len: usize, out: *mut usize, ) -> c_int { ffi_catch!({ - if out.is_null() { return qjd_err::QJD_INVALID_ARG as c_int; } + if out.is_null() { return qjson_err::QJSON_INVALID_ARG as c_int; } match resolve_root_path(doc, path, path_len) { Ok((d, cur)) => match d.cursor_len(cur) { - Ok(n) => { *out = n; qjd_err::QJD_OK as c_int } + Ok(n) => { *out = n; qjson_err::QJSON_OK as c_int } Err(e) => e as c_int, }, Err(e) => e as c_int, @@ -259,39 +261,39 @@ use crate::decode::string; /// writable. /// /// **The returned `(*out_ptr, *out_len)` pair is invalidated by the next -/// `qjd_get_str` / `qjd_cursor_get_str` call on the same document**: the +/// `qjson_get_str` / `qjson_cursor_get_str` call on the same document**: the /// scratch buffer used for escape decoding is reused. Callers must consume /// the result (e.g. `ffi.string(p, n)` in LuaJIT) before issuing another /// string read on the same document. #[no_mangle] -pub unsafe extern "C" fn qjd_get_str( - doc: *mut qjd_doc, path: *const c_char, path_len: usize, +pub unsafe extern "C" fn qjson_get_str( + doc: *mut qjson_doc, path: *const c_char, path_len: usize, out_ptr: *mut *const u8, out_len: *mut usize, ) -> c_int { ffi_catch!({ if out_ptr.is_null() || out_len.is_null() { - return qjd_err::QJD_INVALID_ARG as c_int; + return qjson_err::QJSON_INVALID_ARG as c_int; } let (d, cur) = match resolve_root_path(doc, path, path_len) { Ok(x) => x, Err(e) => return e as c_int, }; let pos = d.indices[cur.idx_start as usize] as usize; if d.buf.get(pos).copied() != Some(b'"') { - return qjd_err::QJD_TYPE_MISMATCH as c_int; + return qjson_err::QJSON_TYPE_MISMATCH as c_int; } // String ends at the close quote, whose indices position is idx_start + 1. let close = d.indices[(cur.idx_start + 1) as usize] as usize; let mut scratch = d.scratch.borrow_mut(); match string::decode_string(d.buf, pos + 1, close, &mut scratch) { - Ok((p, n)) => { *out_ptr = p; *out_len = n; qjd_err::QJD_OK as c_int } + Ok((p, n)) => { *out_ptr = p; *out_len = n; qjson_err::QJSON_OK as c_int } Err(e) => e as c_int, } }) } /// Parse the JSON number at `path` as `i64` and write into `*out`. -/// Returns `QJD_OUT_OF_RANGE` if the value does not fit in `i64`. +/// Returns `QJSON_OUT_OF_RANGE` if the value does not fit in `i64`. /// /// # Safety /// @@ -299,11 +301,11 @@ pub unsafe extern "C" fn qjd_get_str( /// `doc` must be live or NULL; `path` must point to `path_len` bytes or be /// NULL with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] -pub unsafe extern "C" fn qjd_get_i64( - doc: *mut qjd_doc, path: *const c_char, path_len: usize, out: *mut i64, +pub unsafe extern "C" fn qjson_get_i64( + doc: *mut qjson_doc, path: *const c_char, path_len: usize, out: *mut i64, ) -> c_int { ffi_catch!({ - if out.is_null() { return qjd_err::QJD_INVALID_ARG as c_int; } + if out.is_null() { return qjson_err::QJSON_INVALID_ARG as c_int; } let (d, cur) = match resolve_root_path(doc, path, path_len) { Ok(x) => x, Err(e) => return e as c_int, }; @@ -311,7 +313,7 @@ pub unsafe extern "C" fn qjd_get_i64( Ok(b) => b, Err(e) => return e as c_int, }; match number::parse_i64(bytes) { - Ok(v) => { *out = v; qjd_err::QJD_OK as c_int } + Ok(v) => { *out = v; qjson_err::QJSON_OK as c_int } Err(e) => e as c_int, } }) @@ -325,11 +327,11 @@ pub unsafe extern "C" fn qjd_get_i64( /// `doc` must be live or NULL; `path` must point to `path_len` bytes or be /// NULL with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] -pub unsafe extern "C" fn qjd_get_f64( - doc: *mut qjd_doc, path: *const c_char, path_len: usize, out: *mut f64, +pub unsafe extern "C" fn qjson_get_f64( + doc: *mut qjson_doc, path: *const c_char, path_len: usize, out: *mut f64, ) -> c_int { ffi_catch!({ - if out.is_null() { return qjd_err::QJD_INVALID_ARG as c_int; } + if out.is_null() { return qjson_err::QJSON_INVALID_ARG as c_int; } let (d, cur) = match resolve_root_path(doc, path, path_len) { Ok(x) => x, Err(e) => return e as c_int, }; @@ -337,14 +339,14 @@ pub unsafe extern "C" fn qjd_get_f64( Ok(b) => b, Err(e) => return e as c_int, }; match number::parse_f64(bytes) { - Ok(v) => { *out = v; qjd_err::QJD_OK as c_int } + Ok(v) => { *out = v; qjson_err::QJSON_OK as c_int } Err(e) => e as c_int, } }) } /// Write `1` / `0` into `*out` for JSON `true` / `false` at `path`. -/// Returns `QJD_TYPE_MISMATCH` for any other value. +/// Returns `QJSON_TYPE_MISMATCH` for any other value. /// /// # Safety /// @@ -352,11 +354,11 @@ pub unsafe extern "C" fn qjd_get_f64( /// `doc` must be live or NULL; `path` must point to `path_len` bytes or be /// NULL with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] -pub unsafe extern "C" fn qjd_get_bool( - doc: *mut qjd_doc, path: *const c_char, path_len: usize, out: *mut c_int, +pub unsafe extern "C" fn qjson_get_bool( + doc: *mut qjson_doc, path: *const c_char, path_len: usize, out: *mut c_int, ) -> c_int { ffi_catch!({ - if out.is_null() { return qjd_err::QJD_INVALID_ARG as c_int; } + if out.is_null() { return qjson_err::QJSON_INVALID_ARG as c_int; } let (d, cur) = match resolve_root_path(doc, path, path_len) { Ok(x) => x, Err(e) => return e as c_int, }; @@ -364,9 +366,9 @@ pub unsafe extern "C" fn qjd_get_bool( Ok(b) => b, Err(e) => return e as c_int, }; match bytes { - b"true" => { *out = 1; qjd_err::QJD_OK as c_int } - b"false" => { *out = 0; qjd_err::QJD_OK as c_int } - _ => qjd_err::QJD_TYPE_MISMATCH as c_int, + b"true" => { *out = 1; qjson_err::QJSON_OK as c_int } + b"false" => { *out = 0; qjson_err::QJSON_OK as c_int } + _ => qjson_err::QJSON_TYPE_MISMATCH as c_int, } }) } @@ -376,10 +378,10 @@ pub unsafe extern "C" fn qjd_get_bool( /// of the structural char AFTER the scalar (a separator or closer); the /// scalar's bytes sit between `find_scalar_start(cur.idx_start)` and that /// structural char, with trailing whitespace stripped. -unsafe fn scalar_byte_range(d: &Document<'_>, cur: Cursor) -> Result<(usize, usize), qjd_err> { +unsafe fn scalar_byte_range(d: &Document<'_>, cur: Cursor) -> Result<(usize, usize), qjson_err> { let start = d.find_scalar_start(cur.idx_start)?; let end = d.indices[cur.idx_start as usize] as usize; - if end < start { return Err(qjd_err::QJD_PARSE_ERROR); } + if end < start { return Err(qjson_err::QJSON_PARSE_ERROR); } let mut e = end; while e > start && matches!(d.buf[e - 1], b' '|b'\t'|b'\n'|b'\r') { e -= 1; } Ok((start, e)) @@ -388,37 +390,37 @@ unsafe fn scalar_byte_range(d: &Document<'_>, cur: Cursor) -> Result<(usize, usi /// Return the byte slice for a scalar value (number, true, false, null). /// Uses the cursor convention: cur.idx_start is the position in indices of /// the structural char AFTER the scalar (a separator or closer). -unsafe fn scalar_bytes<'d>(d: &'d Document<'d>, cur: Cursor) -> Result<&'d [u8], qjd_err> { +unsafe fn scalar_bytes<'d>(d: &'d Document<'d>, cur: Cursor) -> Result<&'d [u8], qjson_err> { let (s, e) = scalar_byte_range(d, cur)?; Ok(&d.buf[s..e]) } -// ── qjd_cursor type and cursor-based FFI ──────────────────────────────────── +// ── qjson_cursor type and cursor-based FFI ──────────────────────────────────── #[repr(C)] #[derive(Copy, Clone)] -pub struct qjd_cursor { - pub doc: *const qjd_doc, +pub struct qjson_cursor { + pub doc: *const qjson_doc, pub idx_start: u32, pub idx_end: u32, pub _reserved0: u32, pub _reserved1: u32, } -/// Turn a `*const qjd_cursor` into `(&'static Document<'static>, Cursor)` for Rust use. -unsafe fn cursor_to_internal(c: *const qjd_cursor) -> Result<(&'static Document<'static>, Cursor), qjd_err> { - if c.is_null() { return Err(qjd_err::QJD_INVALID_ARG); } +/// Turn a `*const qjson_cursor` into `(&'static Document<'static>, Cursor)` for Rust use. +unsafe fn cursor_to_internal(c: *const qjson_cursor) -> Result<(&'static Document<'static>, Cursor), qjson_err> { + if c.is_null() { return Err(qjson_err::QJSON_INVALID_ARG); } let cc = &*c; - if cc.doc.is_null() { return Err(qjd_err::QJD_INVALID_ARG); } - let d: &Document = &(*(cc.doc as *mut qjd_doc)).0; + if cc.doc.is_null() { return Err(qjson_err::QJSON_INVALID_ARG); } + let d: &Document = &(*(cc.doc as *mut qjson_doc)).0; Ok(( std::mem::transmute::<&Document<'_>, &'static Document<'static>>(d), Cursor { idx_start: cc.idx_start, idx_end: cc.idx_end }, )) } -fn internal_to_cursor(doc: *const qjd_doc, cur: Cursor) -> qjd_cursor { - qjd_cursor { +fn internal_to_cursor(doc: *const qjson_doc, cur: Cursor) -> qjson_cursor { + qjson_cursor { doc, idx_start: cur.idx_start, idx_end: cur.idx_end, @@ -436,15 +438,15 @@ fn internal_to_cursor(doc: *const qjd_doc, cur: Cursor) -> qjd_cursor { /// `doc` must be live or NULL; `path` must point to `path_len` bytes or be /// NULL with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] -pub unsafe extern "C" fn qjd_open( - doc: *mut qjd_doc, path: *const c_char, path_len: usize, out: *mut qjd_cursor, +pub unsafe extern "C" fn qjson_open( + doc: *mut qjson_doc, path: *const c_char, path_len: usize, out: *mut qjson_cursor, ) -> c_int { ffi_catch!({ - if out.is_null() { return qjd_err::QJD_INVALID_ARG as c_int; } + if out.is_null() { return qjson_err::QJSON_INVALID_ARG as c_int; } match resolve_root_path(doc, path, path_len) { Ok((_, cur)) => { - *out = internal_to_cursor(doc as *const qjd_doc, cur); - qjd_err::QJD_OK as c_int + *out = internal_to_cursor(doc as *const qjson_doc, cur); + qjson_err::QJSON_OK as c_int } Err(e) => e as c_int, } @@ -457,21 +459,21 @@ pub unsafe extern "C" fn qjd_open( /// # Safety /// /// See the module-level [shared safety contract](self#shared-safety-contract). -/// `c` must point to a cursor produced by an earlier `qjd_*` call whose +/// `c` must point to a cursor produced by an earlier `qjson_*` call whose /// document is still alive; `path` must point to `path_len` bytes or be NULL /// with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] -pub unsafe extern "C" fn qjd_cursor_open( - c: *const qjd_cursor, path: *const c_char, path_len: usize, out: *mut qjd_cursor, +pub unsafe extern "C" fn qjson_cursor_open( + c: *const qjson_cursor, path: *const c_char, path_len: usize, out: *mut qjson_cursor, ) -> c_int { ffi_catch!({ - if out.is_null() { return qjd_err::QJD_INVALID_ARG as c_int; } + if out.is_null() { return qjson_err::QJSON_INVALID_ARG as c_int; } let (d, cur) = match cursor_to_internal(c) { Ok(x) => x, Err(e) => return e as c_int }; let p: &[u8] = if path.is_null() { &[] } else { std::slice::from_raw_parts(path as *const u8, path_len) }; match cur.resolve(d, p) { - Ok(child) => { *out = internal_to_cursor((*c).doc, child); qjd_err::QJD_OK as c_int } + Ok(child) => { *out = internal_to_cursor((*c).doc, child); qjson_err::QJSON_OK as c_int } Err(e) => e as c_int, } }) @@ -483,16 +485,16 @@ pub unsafe extern "C" fn qjd_cursor_open( /// # Safety /// /// See the module-level [shared safety contract](self#shared-safety-contract). -/// `c` must point to a cursor produced by an earlier `qjd_*` call whose +/// `c` must point to a cursor produced by an earlier `qjson_*` call whose /// document is still alive; `key` must point to `key_len` bytes or be NULL /// with `key_len == 0`; `out` must be non-NULL and writable. #[no_mangle] -pub unsafe extern "C" fn qjd_cursor_field( - c: *const qjd_cursor, key: *const c_char, key_len: usize, out: *mut qjd_cursor, +pub unsafe extern "C" fn qjson_cursor_field( + c: *const qjson_cursor, key: *const c_char, key_len: usize, out: *mut qjson_cursor, ) -> c_int { ffi_catch!({ if out.is_null() || (key.is_null() && key_len != 0) { - return qjd_err::QJD_INVALID_ARG as c_int; + return qjson_err::QJSON_INVALID_ARG as c_int; } let (d, cur) = match cursor_to_internal(c) { Ok(x) => x, Err(e) => return e as c_int }; let k = if key.is_null() { &[][..] } else { std::slice::from_raw_parts(key as *const u8, key_len) }; @@ -500,7 +502,7 @@ pub unsafe extern "C" fn qjd_cursor_field( Ok(x) => x, Err(e) => return e as c_int, }; *out = internal_to_cursor((*c).doc, child); - qjd_err::QJD_OK as c_int + qjson_err::QJSON_OK as c_int }) } @@ -509,21 +511,21 @@ pub unsafe extern "C" fn qjd_cursor_field( /// # Safety /// /// See the module-level [shared safety contract](self#shared-safety-contract). -/// `c` must point to a cursor produced by an earlier `qjd_*` call whose +/// `c` must point to a cursor produced by an earlier `qjson_*` call whose /// document is still alive; `out` must be non-NULL and writable. #[no_mangle] -pub unsafe extern "C" fn qjd_cursor_index( - c: *const qjd_cursor, i: usize, out: *mut qjd_cursor, +pub unsafe extern "C" fn qjson_cursor_index( + c: *const qjson_cursor, i: usize, out: *mut qjson_cursor, ) -> c_int { ffi_catch!({ - if out.is_null() { return qjd_err::QJD_INVALID_ARG as c_int; } - if i > u32::MAX as usize { return qjd_err::QJD_INVALID_ARG as c_int; } + if out.is_null() { return qjson_err::QJSON_INVALID_ARG as c_int; } + if i > u32::MAX as usize { return qjson_err::QJSON_INVALID_ARG as c_int; } let (d, cur) = match cursor_to_internal(c) { Ok(x) => x, Err(e) => return e as c_int }; let child = match crate::cursor::resolve_single_idx(d, cur, i as u32) { Ok(x) => x, Err(e) => return e as c_int, }; *out = internal_to_cursor((*c).doc, child); - qjd_err::QJD_OK as c_int + qjson_err::QJSON_OK as c_int }) } @@ -532,22 +534,22 @@ pub unsafe extern "C" fn qjd_cursor_index( /// # Safety /// /// See the module-level [shared safety contract](self#shared-safety-contract). -/// `c` must point to a cursor produced by an earlier `qjd_*` call whose +/// `c` must point to a cursor produced by an earlier `qjson_*` call whose /// document is still alive; `path` must point to `path_len` bytes or be NULL /// with `path_len == 0`; `out_ptr` and `out_len` must be non-NULL and /// writable. /// /// **The returned `(*out_ptr, *out_len)` pair is invalidated by the next -/// `qjd_get_str` / `qjd_cursor_get_str` call on the same document** (scratch -/// buffer reuse). See [`qjd_get_str`]. +/// `qjson_get_str` / `qjson_cursor_get_str` call on the same document** (scratch +/// buffer reuse). See [`qjson_get_str`]. #[no_mangle] -pub unsafe extern "C" fn qjd_cursor_get_str( - c: *const qjd_cursor, path: *const c_char, path_len: usize, +pub unsafe extern "C" fn qjson_cursor_get_str( + c: *const qjson_cursor, path: *const c_char, path_len: usize, out_ptr: *mut *const u8, out_len: *mut usize, ) -> c_int { ffi_catch!({ if out_ptr.is_null() || out_len.is_null() { - return qjd_err::QJD_INVALID_ARG as c_int; + return qjson_err::QJSON_INVALID_ARG as c_int; } let (d, cur) = match cursor_to_internal(c) { Ok(x) => x, Err(e) => return e as c_int }; let p: &[u8] = if path.is_null() { &[] } else { @@ -556,33 +558,33 @@ pub unsafe extern "C" fn qjd_cursor_get_str( let cur = match cur.resolve(d, p) { Ok(x) => x, Err(e) => return e as c_int }; let pos = d.indices[cur.idx_start as usize] as usize; if d.buf.get(pos).copied() != Some(b'"') { - return qjd_err::QJD_TYPE_MISMATCH as c_int; + return qjson_err::QJSON_TYPE_MISMATCH as c_int; } let close = d.indices[(cur.idx_start + 1) as usize] as usize; let mut scratch = d.scratch.borrow_mut(); match string::decode_string(d.buf, pos + 1, close, &mut scratch) { - Ok((p, n)) => { *out_ptr = p; *out_len = n; qjd_err::QJD_OK as c_int } + Ok((p, n)) => { *out_ptr = p; *out_len = n; qjson_err::QJSON_OK as c_int } Err(e) => e as c_int, } }) } /// Parse the JSON number at `path` (relative to `*c`) as `i64`. -/// Returns `QJD_OUT_OF_RANGE` if the value does not fit in `i64`. +/// Returns `QJSON_OUT_OF_RANGE` if the value does not fit in `i64`. /// /// # Safety /// /// See the module-level [shared safety contract](self#shared-safety-contract). -/// `c` must point to a cursor produced by an earlier `qjd_*` call whose +/// `c` must point to a cursor produced by an earlier `qjson_*` call whose /// document is still alive; `path` must point to `path_len` bytes or be NULL /// with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] -pub unsafe extern "C" fn qjd_cursor_get_i64( - c: *const qjd_cursor, path: *const c_char, path_len: usize, out: *mut i64, +pub unsafe extern "C" fn qjson_cursor_get_i64( + c: *const qjson_cursor, path: *const c_char, path_len: usize, out: *mut i64, ) -> c_int { ffi_catch!({ - if out.is_null() { return qjd_err::QJD_INVALID_ARG as c_int; } + if out.is_null() { return qjson_err::QJSON_INVALID_ARG as c_int; } let (d, cur) = match cursor_to_internal(c) { Ok(x) => x, Err(e) => return e as c_int }; let p: &[u8] = if path.is_null() { &[] } else { std::slice::from_raw_parts(path as *const u8, path_len) @@ -590,7 +592,7 @@ pub unsafe extern "C" fn qjd_cursor_get_i64( let cur = match cur.resolve(d, p) { Ok(x) => x, Err(e) => return e as c_int }; let bytes = match scalar_bytes(d, cur) { Ok(b) => b, Err(e) => return e as c_int }; match number::parse_i64(bytes) { - Ok(v) => { *out = v; qjd_err::QJD_OK as c_int } + Ok(v) => { *out = v; qjson_err::QJSON_OK as c_int } Err(e) => e as c_int, } }) @@ -601,15 +603,15 @@ pub unsafe extern "C" fn qjd_cursor_get_i64( /// # Safety /// /// See the module-level [shared safety contract](self#shared-safety-contract). -/// `c` must point to a cursor produced by an earlier `qjd_*` call whose +/// `c` must point to a cursor produced by an earlier `qjson_*` call whose /// document is still alive; `path` must point to `path_len` bytes or be NULL /// with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] -pub unsafe extern "C" fn qjd_cursor_get_f64( - c: *const qjd_cursor, path: *const c_char, path_len: usize, out: *mut f64, +pub unsafe extern "C" fn qjson_cursor_get_f64( + c: *const qjson_cursor, path: *const c_char, path_len: usize, out: *mut f64, ) -> c_int { ffi_catch!({ - if out.is_null() { return qjd_err::QJD_INVALID_ARG as c_int; } + if out.is_null() { return qjson_err::QJSON_INVALID_ARG as c_int; } let (d, cur) = match cursor_to_internal(c) { Ok(x) => x, Err(e) => return e as c_int }; let p: &[u8] = if path.is_null() { &[] } else { std::slice::from_raw_parts(path as *const u8, path_len) @@ -617,27 +619,27 @@ pub unsafe extern "C" fn qjd_cursor_get_f64( let cur = match cur.resolve(d, p) { Ok(x) => x, Err(e) => return e as c_int }; let bytes = match scalar_bytes(d, cur) { Ok(b) => b, Err(e) => return e as c_int }; match number::parse_f64(bytes) { - Ok(v) => { *out = v; qjd_err::QJD_OK as c_int } + Ok(v) => { *out = v; qjson_err::QJSON_OK as c_int } Err(e) => e as c_int, } }) } /// Write `1` / `0` into `*out` for JSON `true` / `false` at `path` -/// (relative to `*c`). Returns `QJD_TYPE_MISMATCH` for any other value. +/// (relative to `*c`). Returns `QJSON_TYPE_MISMATCH` for any other value. /// /// # Safety /// /// See the module-level [shared safety contract](self#shared-safety-contract). -/// `c` must point to a cursor produced by an earlier `qjd_*` call whose +/// `c` must point to a cursor produced by an earlier `qjson_*` call whose /// document is still alive; `path` must point to `path_len` bytes or be NULL /// with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] -pub unsafe extern "C" fn qjd_cursor_get_bool( - c: *const qjd_cursor, path: *const c_char, path_len: usize, out: *mut c_int, +pub unsafe extern "C" fn qjson_cursor_get_bool( + c: *const qjson_cursor, path: *const c_char, path_len: usize, out: *mut c_int, ) -> c_int { ffi_catch!({ - if out.is_null() { return qjd_err::QJD_INVALID_ARG as c_int; } + if out.is_null() { return qjson_err::QJSON_INVALID_ARG as c_int; } let (d, cur) = match cursor_to_internal(c) { Ok(x) => x, Err(e) => return e as c_int }; let p: &[u8] = if path.is_null() { &[] } else { std::slice::from_raw_parts(path as *const u8, path_len) @@ -645,9 +647,9 @@ pub unsafe extern "C" fn qjd_cursor_get_bool( let cur = match cur.resolve(d, p) { Ok(x) => x, Err(e) => return e as c_int }; let bytes = match scalar_bytes(d, cur) { Ok(b) => b, Err(e) => return e as c_int }; match bytes { - b"true" => { *out = 1; qjd_err::QJD_OK as c_int } - b"false" => { *out = 0; qjd_err::QJD_OK as c_int } - _ => qjd_err::QJD_TYPE_MISMATCH as c_int, + b"true" => { *out = 1; qjson_err::QJSON_OK as c_int } + b"false" => { *out = 0; qjson_err::QJSON_OK as c_int } + _ => qjson_err::QJSON_TYPE_MISMATCH as c_int, } }) } @@ -657,49 +659,49 @@ pub unsafe extern "C" fn qjd_cursor_get_bool( /// # Safety /// /// See the module-level [shared safety contract](self#shared-safety-contract). -/// `c` must point to a cursor produced by an earlier `qjd_*` call whose +/// `c` must point to a cursor produced by an earlier `qjson_*` call whose /// document is still alive; `path` must point to `path_len` bytes or be NULL /// with `path_len == 0`; `type_out` must be non-NULL and writable. #[no_mangle] -pub unsafe extern "C" fn qjd_cursor_typeof( - c: *const qjd_cursor, path: *const c_char, path_len: usize, type_out: *mut c_int, +pub unsafe extern "C" fn qjson_cursor_typeof( + c: *const qjson_cursor, path: *const c_char, path_len: usize, type_out: *mut c_int, ) -> c_int { ffi_catch!({ - if type_out.is_null() { return qjd_err::QJD_INVALID_ARG as c_int; } + if type_out.is_null() { return qjson_err::QJSON_INVALID_ARG as c_int; } let (d, cur) = match cursor_to_internal(c) { Ok(x) => x, Err(e) => return e as c_int }; let p: &[u8] = if path.is_null() { &[] } else { std::slice::from_raw_parts(path as *const u8, path_len) }; let cur = match cur.resolve(d, p) { Ok(x) => x, Err(e) => return e as c_int }; match d.type_of(cur) { - Ok(t) => { *type_out = t as c_int; qjd_err::QJD_OK as c_int } + Ok(t) => { *type_out = t as c_int; qjson_err::QJSON_OK as c_int } Err(e) => e as c_int, } }) } /// Write the number of direct children of the container at `path` -/// (relative to `*c`) into `*out`. Returns `QJD_TYPE_MISMATCH` for non-containers. +/// (relative to `*c`) into `*out`. Returns `QJSON_TYPE_MISMATCH` for non-containers. /// /// # Safety /// /// See the module-level [shared safety contract](self#shared-safety-contract). -/// `c` must point to a cursor produced by an earlier `qjd_*` call whose +/// `c` must point to a cursor produced by an earlier `qjson_*` call whose /// document is still alive; `path` must point to `path_len` bytes or be NULL /// with `path_len == 0`; `out` must be non-NULL and writable. #[no_mangle] -pub unsafe extern "C" fn qjd_cursor_len( - c: *const qjd_cursor, path: *const c_char, path_len: usize, out: *mut usize, +pub unsafe extern "C" fn qjson_cursor_len( + c: *const qjson_cursor, path: *const c_char, path_len: usize, out: *mut usize, ) -> c_int { ffi_catch!({ - if out.is_null() { return qjd_err::QJD_INVALID_ARG as c_int; } + if out.is_null() { return qjson_err::QJSON_INVALID_ARG as c_int; } let (d, cur) = match cursor_to_internal(c) { Ok(x) => x, Err(e) => return e as c_int }; let p: &[u8] = if path.is_null() { &[] } else { std::slice::from_raw_parts(path as *const u8, path_len) }; let cur = match cur.resolve(d, p) { Ok(x) => x, Err(e) => return e as c_int }; match d.cursor_len(cur) { - Ok(n) => { *out = n; qjd_err::QJD_OK as c_int } + Ok(n) => { *out = n; qjson_err::QJSON_OK as c_int } Err(e) => e as c_int, } }) @@ -714,16 +716,16 @@ pub unsafe extern "C" fn qjd_cursor_len( /// # Safety /// /// See the module-level [shared safety contract](self#shared-safety-contract). -/// `c` must point to a cursor produced by an earlier `qjd_*` call whose +/// `c` must point to a cursor produced by an earlier `qjson_*` call whose /// document is still alive; `byte_start` and `byte_end` must be non-NULL /// and writable. #[no_mangle] -pub unsafe extern "C" fn qjd_cursor_bytes( - c: *const qjd_cursor, byte_start: *mut usize, byte_end: *mut usize, +pub unsafe extern "C" fn qjson_cursor_bytes( + c: *const qjson_cursor, byte_start: *mut usize, byte_end: *mut usize, ) -> c_int { ffi_catch!({ if byte_start.is_null() || byte_end.is_null() { - return qjd_err::QJD_INVALID_ARG as c_int; + return qjson_err::QJSON_INVALID_ARG as c_int; } let (d, cur) = match cursor_to_internal(c) { Ok(x) => x, Err(e) => return e as c_int, @@ -731,7 +733,7 @@ pub unsafe extern "C" fn qjd_cursor_bytes( let pos = d.indices[cur.idx_start as usize] as usize; let lead = match d.buf.get(pos) { Some(b) => *b, - None => return qjd_err::QJD_PARSE_ERROR as c_int, + None => return qjson_err::QJSON_PARSE_ERROR as c_int, }; match lead { b'{' | b'[' | b'"' => { @@ -739,11 +741,11 @@ pub unsafe extern "C" fn qjd_cursor_bytes( // closer, inclusive. let end = d.indices[cur.idx_end as usize] as usize; if end >= d.buf.len() { - return qjd_err::QJD_PARSE_ERROR as c_int; + return qjson_err::QJSON_PARSE_ERROR as c_int; } *byte_start = pos; *byte_end = end + 1; - qjd_err::QJD_OK as c_int + qjson_err::QJSON_OK as c_int } _ => { // Scalar: delegate to scalar_byte_range. @@ -752,7 +754,7 @@ pub unsafe extern "C" fn qjd_cursor_bytes( }; *byte_start = s; *byte_end = e; - qjd_err::QJD_OK as c_int + qjson_err::QJSON_OK as c_int } } }) @@ -761,25 +763,25 @@ pub unsafe extern "C" fn qjd_cursor_bytes( /// Write the i-th object entry's key (decoded into the doc's scratch /// buffer) and value cursor into the out parameters. /// -/// Returns `QJD_TYPE_MISMATCH` when the cursor is not an object, or -/// `QJD_NOT_FOUND` when `i` is past the end. +/// Returns `QJSON_TYPE_MISMATCH` when the cursor is not an object, or +/// `QJSON_NOT_FOUND` when `i` is past the end. /// /// # Safety /// /// See the module-level [shared safety contract](self#shared-safety-contract). /// `c` must point to a live cursor; `key_ptr`, `key_len`, and `value_out` /// must be non-NULL and writable. The `(*key_ptr, *key_len)` pair is -/// invalidated by the next `qjd_get_str` / `qjd_cursor_get_str` / -/// `qjd_cursor_object_entry_at` call on the same document (scratch reuse). +/// invalidated by the next `qjson_get_str` / `qjson_cursor_get_str` / +/// `qjson_cursor_object_entry_at` call on the same document (scratch reuse). #[no_mangle] -pub unsafe extern "C" fn qjd_cursor_object_entry_at( - c: *const qjd_cursor, i: usize, +pub unsafe extern "C" fn qjson_cursor_object_entry_at( + c: *const qjson_cursor, i: usize, key_ptr: *mut *const u8, key_len: *mut usize, - value_out: *mut qjd_cursor, + value_out: *mut qjson_cursor, ) -> c_int { ffi_catch!({ if key_ptr.is_null() || key_len.is_null() || value_out.is_null() { - return qjd_err::QJD_INVALID_ARG as c_int; + return qjson_err::QJSON_INVALID_ARG as c_int; } let (d, cur) = match cursor_to_internal(c) { Ok(x) => x, Err(e) => return e as c_int, @@ -797,7 +799,7 @@ pub unsafe extern "C" fn qjd_cursor_object_entry_at( *key_ptr = p; *key_len = n; *value_out = internal_to_cursor((*c).doc, value_cur); - qjd_err::QJD_OK as c_int + qjson_err::QJSON_OK as c_int } Err(e) => e as c_int, } @@ -805,14 +807,14 @@ pub unsafe extern "C" fn qjd_cursor_object_entry_at( } /// Test-only export that forces a Rust panic to verify the FFI panic barrier -/// converts it to `QJD_OOM` instead of unwinding across the boundary. +/// converts it to `QJSON_OOM` instead of unwinding across the boundary. /// /// # Safety /// /// Has no preconditions. Marked `unsafe extern "C"` for ABI consistency. #[cfg(feature = "test-panic")] #[no_mangle] -pub unsafe extern "C" fn qjd_test_panic() -> c_int { +pub unsafe extern "C" fn qjson_test_panic() -> c_int { ffi_catch!({ panic!("forced panic for test"); }) diff --git a/src/lib.rs b/src/lib.rs index 87f5c6a..f75bb53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -//! lua-quick-decode: Rust JSON decoder for LuaJIT FFI consumers. +//! qjson: Rust JSON decoder for LuaJIT FFI consumers. pub mod error; pub mod options; diff --git a/src/options.rs b/src/options.rs index 3c1241c..827e236 100644 --- a/src/options.rs +++ b/src/options.rs @@ -1,38 +1,38 @@ #![allow(non_camel_case_types)] -pub const QJD_MODE_EAGER: u32 = 0; -pub const QJD_MODE_LAZY: u32 = 1; -pub const QJD_DEFAULT_MAX_DEPTH: u32 = 1024; -pub const QJD_MAX_MAX_DEPTH: u32 = 4096; +pub const QJSON_MODE_EAGER: u32 = 0; +pub const QJSON_MODE_LAZY: u32 = 1; +pub const QJSON_DEFAULT_MAX_DEPTH: u32 = 1024; +pub const QJSON_MAX_MAX_DEPTH: u32 = 4096; /// Caller-visible parse options. Layout is FFI-stable: kept in sync with -/// `qjd_options` in `include/lua_quick_decode.h`. +/// `qjson_options` in `include/qjson.h`. #[repr(C)] #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct Options { - /// `QJD_MODE_EAGER` (0) — full RFC 8259 validation during parse. - /// `QJD_MODE_LAZY` (1) — structural-only; defer value errors to access. + /// `QJSON_MODE_EAGER` (0) — full RFC 8259 validation during parse. + /// `QJSON_MODE_LAZY` (1) — structural-only; defer value errors to access. pub mode: u32, - /// Max bracket nesting depth. `0` selects `QJD_DEFAULT_MAX_DEPTH` (1024). - /// Values >`QJD_MAX_MAX_DEPTH` are clamped to that ceiling. + /// Max bracket nesting depth. `0` selects `QJSON_DEFAULT_MAX_DEPTH` (1024). + /// Values >`QJSON_MAX_MAX_DEPTH` are clamped to that ceiling. pub max_depth: u32, } impl Default for Options { fn default() -> Self { - Self { mode: QJD_MODE_EAGER, max_depth: 0 } + Self { mode: QJSON_MODE_EAGER, max_depth: 0 } } } #[allow(dead_code)] // used in Task 6+ validators impl Options { pub(crate) fn effective_max_depth(&self) -> u32 { - let d = if self.max_depth == 0 { QJD_DEFAULT_MAX_DEPTH } else { self.max_depth }; - d.min(QJD_MAX_MAX_DEPTH) + let d = if self.max_depth == 0 { QJSON_DEFAULT_MAX_DEPTH } else { self.max_depth }; + d.min(QJSON_MAX_MAX_DEPTH) } pub(crate) fn is_eager(&self) -> bool { - self.mode == QJD_MODE_EAGER + self.mode == QJSON_MODE_EAGER } } @@ -44,12 +44,12 @@ mod tests { #[test] fn zero_max_depth_falls_back_to_default() { - assert_eq!(Options::default().effective_max_depth(), QJD_DEFAULT_MAX_DEPTH); + assert_eq!(Options::default().effective_max_depth(), QJSON_DEFAULT_MAX_DEPTH); } #[test] fn huge_max_depth_is_clamped() { let o = Options { mode: 0, max_depth: u32::MAX }; - assert_eq!(o.effective_max_depth(), QJD_MAX_MAX_DEPTH); + assert_eq!(o.effective_max_depth(), QJSON_MAX_MAX_DEPTH); } } diff --git a/src/path.rs b/src/path.rs index 35f67a0..0b66564 100644 --- a/src/path.rs +++ b/src/path.rs @@ -1,4 +1,4 @@ -use crate::error::qjd_err; +use crate::error::qjson_err; #[derive(Debug, PartialEq, Eq)] pub(crate) enum PathSeg<'a> { @@ -15,7 +15,7 @@ impl<'a> PathIter<'a> { } impl<'a> Iterator for PathIter<'a> { - type Item = Result, qjd_err>; + type Item = Result, qjson_err>; fn next(&mut self) -> Option { if self.rest.is_empty() { @@ -28,11 +28,11 @@ impl<'a> Iterator for PathIter<'a> { // Index segment: [digits] let close = match self.rest.iter().position(|&c| c == b']') { Some(p) => p, - None => return Some(Err(qjd_err::QJD_INVALID_PATH)), + None => return Some(Err(qjson_err::QJSON_INVALID_PATH)), }; let digits = &self.rest[1..close]; if digits.is_empty() || !digits.iter().all(|c| c.is_ascii_digit()) { - return Some(Err(qjd_err::QJD_INVALID_PATH)); + return Some(Err(qjson_err::QJSON_INVALID_PATH)); } let mut n: u32 = 0; for &c in digits { @@ -40,7 +40,7 @@ impl<'a> Iterator for PathIter<'a> { .and_then(|x| x.checked_add((c - b'0') as u32)) { Some(v) => v, - None => return Some(Err(qjd_err::QJD_INVALID_PATH)), + None => return Some(Err(qjson_err::QJSON_INVALID_PATH)), }; } self.rest = &self.rest[close + 1..]; @@ -51,7 +51,7 @@ impl<'a> Iterator for PathIter<'a> { // Separator before a key. Skip it then require a key. self.rest = &self.rest[1..]; if self.rest.is_empty() { - return Some(Err(qjd_err::QJD_INVALID_PATH)); + return Some(Err(qjson_err::QJSON_INVALID_PATH)); } return self.next(); } @@ -61,7 +61,7 @@ impl<'a> Iterator for PathIter<'a> { .position(|&c| c == b'.' || c == b'[') .unwrap_or(self.rest.len()); if end == 0 { - return Some(Err(qjd_err::QJD_INVALID_PATH)); + return Some(Err(qjson_err::QJSON_INVALID_PATH)); } let key = &self.rest[..end]; self.rest = &self.rest[end..]; @@ -73,7 +73,7 @@ impl<'a> Iterator for PathIter<'a> { mod tests { use super::*; - fn parse(p: &[u8]) -> Result>, qjd_err> { + fn parse(p: &[u8]) -> Result>, qjson_err> { PathIter::new(p).collect() } @@ -131,16 +131,16 @@ mod tests { #[test] fn unterminated_index_is_error() { - assert_eq!(parse(b"a[3"), Err(qjd_err::QJD_INVALID_PATH)); + assert_eq!(parse(b"a[3"), Err(qjson_err::QJSON_INVALID_PATH)); } #[test] fn non_digit_in_index_is_error() { - assert_eq!(parse(b"a[abc]"), Err(qjd_err::QJD_INVALID_PATH)); + assert_eq!(parse(b"a[abc]"), Err(qjson_err::QJSON_INVALID_PATH)); } #[test] fn trailing_dot_is_error() { - assert_eq!(parse(b"a."), Err(qjd_err::QJD_INVALID_PATH)); + assert_eq!(parse(b"a."), Err(qjson_err::QJSON_INVALID_PATH)); } } diff --git a/src/validate/mod.rs b/src/validate/mod.rs index 8ddee23..366e518 100644 --- a/src/validate/mod.rs +++ b/src/validate/mod.rs @@ -10,7 +10,7 @@ pub(crate) use number::validate_number; pub(crate) mod strings; pub(crate) use strings::validate_string_span; -use crate::error::qjd_err; +use crate::error::qjson_err; /// Verify that the maximum bracket-stack depth implied by `indices` /// does not exceed `max_depth`. Walks indices once; assumes scan() has @@ -21,7 +21,7 @@ pub(crate) fn validate_depth( buf: &[u8], indices: &[u32], max_depth: u32, -) -> Result<(), qjd_err> { +) -> Result<(), qjson_err> { let mut depth: u32 = 0; for &idx in indices { if idx == u32::MAX { break; } @@ -29,7 +29,7 @@ pub(crate) fn validate_depth( b'{' | b'[' => { depth += 1; if depth > max_depth { - return Err(qjd_err::QJD_NESTING_TOO_DEEP); + return Err(qjson_err::QJSON_NESTING_TOO_DEEP); } } b'}' | b']' => { @@ -51,7 +51,7 @@ pub(crate) fn validate_depth( pub(crate) fn validate_trailing( buf: &[u8], indices: &[u32], -) -> Result<(), qjd_err> { +) -> Result<(), qjson_err> { // Find the first real structural character to determine root kind. let first = indices.iter().find(|&&i| i != u32::MAX).copied(); @@ -120,7 +120,7 @@ pub(crate) fn validate_trailing( }; if root_end < buf.len() { - return Err(qjd_err::QJD_TRAILING_CONTENT); + return Err(qjson_err::QJSON_TRAILING_CONTENT); } Ok(()) } @@ -133,7 +133,7 @@ pub(crate) fn validate_trailing( /// value is required (`[,]`, `{"a":}`), missing colons (`{"a"}`), /// missing commas (`{"a":1"b":2}`), non-string object keys (`{1:1}`), /// and stray structural tokens (`[1:2]`) all surface here as -/// `QJD_PARSE_ERROR`. +/// `QJSON_PARSE_ERROR`. /// /// Scalar tokens (numbers, `true`, `false`, `null`) live in the byte /// gap before the *next* structural offset. They are dispatched to @@ -143,7 +143,7 @@ pub(crate) fn validate_trailing( pub(crate) fn validate_eager_values( buf: &[u8], indices: &[u32], -) -> Result<(), qjd_err> { +) -> Result<(), qjson_err> { // Stack of container contexts; the top is the current state. // We use a single seed entry `CtxKind::Top` for the root value. let mut stack: Vec = Vec::with_capacity(16); @@ -183,44 +183,44 @@ pub(crate) fn validate_eager_values( CtxKind::ArrAfterOpen }); } - _ => return Err(qjd_err::QJD_PARSE_ERROR), + _ => return Err(qjson_err::QJSON_PARSE_ERROR), } prev_end = pos + 1; i += 1; } b'}' => { - let top = stack.pop().ok_or(qjd_err::QJD_PARSE_ERROR)?; + let top = stack.pop().ok_or(qjson_err::QJSON_PARSE_ERROR)?; if !matches!(top, CtxKind::ObjAfterOpen | CtxKind::ObjAfterValue) { - return Err(qjd_err::QJD_PARSE_ERROR); + return Err(qjson_err::QJSON_PARSE_ERROR); } - if stack.is_empty() { return Err(qjd_err::QJD_PARSE_ERROR); } + if stack.is_empty() { return Err(qjson_err::QJSON_PARSE_ERROR); } prev_end = pos + 1; i += 1; } b']' => { - let top = stack.pop().ok_or(qjd_err::QJD_PARSE_ERROR)?; + let top = stack.pop().ok_or(qjson_err::QJSON_PARSE_ERROR)?; if !matches!(top, CtxKind::ArrAfterOpen | CtxKind::ArrAfterValue) { - return Err(qjd_err::QJD_PARSE_ERROR); + return Err(qjson_err::QJSON_PARSE_ERROR); } - if stack.is_empty() { return Err(qjd_err::QJD_PARSE_ERROR); } + if stack.is_empty() { return Err(qjson_err::QJSON_PARSE_ERROR); } prev_end = pos + 1; i += 1; } b',' => { - let cur = stack.last_mut().ok_or(qjd_err::QJD_PARSE_ERROR)?; + let cur = stack.last_mut().ok_or(qjson_err::QJSON_PARSE_ERROR)?; match *cur { CtxKind::ArrAfterValue => *cur = CtxKind::ArrAfterComma, CtxKind::ObjAfterValue => *cur = CtxKind::ObjAfterComma, - _ => return Err(qjd_err::QJD_PARSE_ERROR), + _ => return Err(qjson_err::QJSON_PARSE_ERROR), } prev_end = pos + 1; i += 1; } b':' => { - let cur = stack.last_mut().ok_or(qjd_err::QJD_PARSE_ERROR)?; + let cur = stack.last_mut().ok_or(qjson_err::QJSON_PARSE_ERROR)?; match *cur { CtxKind::ObjAfterKey => *cur = CtxKind::ObjAfterColon, - _ => return Err(qjd_err::QJD_PARSE_ERROR), + _ => return Err(qjson_err::QJSON_PARSE_ERROR), } prev_end = pos + 1; i += 1; @@ -228,14 +228,14 @@ pub(crate) fn validate_eager_values( b'"' => { // The scanner pairs the opening and closing quotes; the // closing quote is at indices[i + 1]. - if i + 1 >= indices.len() { return Err(qjd_err::QJD_PARSE_ERROR); } + if i + 1 >= indices.len() { return Err(qjson_err::QJSON_PARSE_ERROR); } let close = indices[i + 1] as usize; if close <= pos || close >= buf.len() || buf[close] != b'"' { - return Err(qjd_err::QJD_PARSE_ERROR); + return Err(qjson_err::QJSON_PARSE_ERROR); } strings::validate_string_span(&buf[pos + 1 .. close])?; - let cur = stack.last_mut().ok_or(qjd_err::QJD_PARSE_ERROR)?; + let cur = stack.last_mut().ok_or(qjson_err::QJSON_PARSE_ERROR)?; match *cur { // Key position in an object. CtxKind::ObjAfterOpen | CtxKind::ObjAfterComma => { @@ -248,12 +248,12 @@ pub(crate) fn validate_eager_values( | CtxKind::ObjAfterColon => { *cur = parent_after_value(*cur); } - _ => return Err(qjd_err::QJD_PARSE_ERROR), + _ => return Err(qjson_err::QJSON_PARSE_ERROR), } prev_end = close + 1; i += 2; } - _ => return Err(qjd_err::QJD_PARSE_ERROR), + _ => return Err(qjson_err::QJSON_PARSE_ERROR), } } @@ -265,7 +265,7 @@ pub(crate) fn validate_eager_values( // After the walk, the stack must hold exactly one frame: the root // context, which must be `TopDone` (root value consumed). if stack.len() != 1 || stack[0] != CtxKind::TopDone { - return Err(qjd_err::QJD_PARSE_ERROR); + return Err(qjson_err::QJSON_PARSE_ERROR); } Ok(()) } @@ -309,7 +309,7 @@ fn consume_scalar_gap( start: usize, end: usize, state: &mut CtxKind, -) -> Result<(), qjd_err> { +) -> Result<(), qjson_err> { // Strip whitespace. let mut s = start; while s < end && is_ws(buf[s]) { s += 1; } @@ -330,7 +330,7 @@ fn consume_scalar_gap( | CtxKind::ArrAfterComma | CtxKind::ObjAfterColon ) { - return Err(qjd_err::QJD_PARSE_ERROR); + return Err(qjson_err::QJSON_PARSE_ERROR); } validate_scalar(&buf[s..e])?; @@ -341,17 +341,17 @@ fn consume_scalar_gap( /// Dispatch a non-empty whitespace-trimmed scalar token to its /// grammar validator. Mirrors the previous `check_gap` precedence: /// - `true` / `false` / `null` exact → Ok -/// - `NaN` / `Infinity` → `QJD_INVALID_NUMBER` (via validate_number) +/// - `NaN` / `Infinity` → `QJSON_INVALID_NUMBER` (via validate_number) /// - `-` / digit / `+` / `.` → `validate_number` -/// - Else → `QJD_PARSE_ERROR` -fn validate_scalar(scalar: &[u8]) -> Result<(), qjd_err> { +/// - Else → `QJSON_PARSE_ERROR` +fn validate_scalar(scalar: &[u8]) -> Result<(), qjson_err> { match scalar[0] { - b't' => if scalar == b"true" { Ok(()) } else { Err(qjd_err::QJD_PARSE_ERROR) }, - b'f' => if scalar == b"false" { Ok(()) } else { Err(qjd_err::QJD_PARSE_ERROR) }, - b'n' => if scalar == b"null" { Ok(()) } else { Err(qjd_err::QJD_PARSE_ERROR) }, + b't' => if scalar == b"true" { Ok(()) } else { Err(qjson_err::QJSON_PARSE_ERROR) }, + b'f' => if scalar == b"false" { Ok(()) } else { Err(qjson_err::QJSON_PARSE_ERROR) }, + b'n' => if scalar == b"null" { Ok(()) } else { Err(qjson_err::QJSON_PARSE_ERROR) }, b'-' | b'0'..=b'9' | b'+' | b'.' => number::validate_number(scalar), _ if scalar == b"NaN" || scalar == b"Infinity" => number::validate_number(scalar), - _ => Err(qjd_err::QJD_PARSE_ERROR), + _ => Err(qjson_err::QJSON_PARSE_ERROR), } } @@ -382,7 +382,7 @@ mod tests { let buf = b"[[[1]]]"; assert_eq!( validate_depth(buf, &ix(buf), 2), - Err(qjd_err::QJD_NESTING_TOO_DEEP), + Err(qjson_err::QJSON_NESTING_TOO_DEEP), ); } @@ -403,7 +403,7 @@ mod tests { let buf = b"{}garbage"; assert_eq!( validate_trailing(buf, &ix(buf)), - Err(qjd_err::QJD_TRAILING_CONTENT), + Err(qjson_err::QJSON_TRAILING_CONTENT), ); } @@ -418,7 +418,7 @@ mod tests { let buf = b"1 2"; assert_eq!( validate_trailing(buf, &ix(buf)), - Err(qjd_err::QJD_TRAILING_CONTENT), + Err(qjson_err::QJSON_TRAILING_CONTENT), ); } @@ -447,42 +447,42 @@ mod tests { #[test] fn grammar_rejects_missing_colon() { let buf = b"{\"a\"}"; - assert_eq!(validate_eager_values(buf, &ix(buf)), Err(qjd_err::QJD_PARSE_ERROR)); + assert_eq!(validate_eager_values(buf, &ix(buf)), Err(qjson_err::QJSON_PARSE_ERROR)); } #[test] fn grammar_rejects_leading_comma_with_value() { let buf = b"[,1]"; - assert_eq!(validate_eager_values(buf, &ix(buf)), Err(qjd_err::QJD_PARSE_ERROR)); + assert_eq!(validate_eager_values(buf, &ix(buf)), Err(qjson_err::QJSON_PARSE_ERROR)); } #[test] fn grammar_rejects_missing_comma_in_object() { let buf = b"{\"a\":1\"b\":2}"; - assert_eq!(validate_eager_values(buf, &ix(buf)), Err(qjd_err::QJD_PARSE_ERROR)); + assert_eq!(validate_eager_values(buf, &ix(buf)), Err(qjson_err::QJSON_PARSE_ERROR)); } #[test] fn grammar_rejects_non_string_object_key() { let buf = b"{1:1}"; - assert_eq!(validate_eager_values(buf, &ix(buf)), Err(qjd_err::QJD_PARSE_ERROR)); + assert_eq!(validate_eager_values(buf, &ix(buf)), Err(qjson_err::QJSON_PARSE_ERROR)); } #[test] fn grammar_rejects_colon_in_array() { let buf = b"[1:2]"; - assert_eq!(validate_eager_values(buf, &ix(buf)), Err(qjd_err::QJD_PARSE_ERROR)); + assert_eq!(validate_eager_values(buf, &ix(buf)), Err(qjson_err::QJSON_PARSE_ERROR)); } #[test] fn grammar_rejects_missing_comma_between_arrays() { let buf = b"[3[4]]"; - assert_eq!(validate_eager_values(buf, &ix(buf)), Err(qjd_err::QJD_PARSE_ERROR)); + assert_eq!(validate_eager_values(buf, &ix(buf)), Err(qjson_err::QJSON_PARSE_ERROR)); } #[test] fn grammar_rejects_trailing_garbage_inside_object() { let buf = b"{\"a\":\"a\" 123}"; - assert_eq!(validate_eager_values(buf, &ix(buf)), Err(qjd_err::QJD_PARSE_ERROR)); + assert_eq!(validate_eager_values(buf, &ix(buf)), Err(qjson_err::QJSON_PARSE_ERROR)); } } diff --git a/src/validate/number.rs b/src/validate/number.rs index c212bdb..04e0ed4 100644 --- a/src/validate/number.rs +++ b/src/validate/number.rs @@ -1,13 +1,13 @@ //! Strict RFC 8259 §6 number-format validation. -use crate::error::qjd_err; +use crate::error::qjson_err; /// Returns Ok if `bytes` matches the JSON `number` grammar exactly. -/// Otherwise returns `QJD_INVALID_NUMBER`. +/// Otherwise returns `QJSON_INVALID_NUMBER`. /// /// Out-of-range (i.e. f64 overflow) is NOT detected here; the f64 decode -/// step surfaces it as `QJD_NUMBER_OUT_OF_RANGE`. -pub(crate) fn validate_number(bytes: &[u8]) -> Result<(), qjd_err> { +/// step surfaces it as `QJSON_NUMBER_OUT_OF_RANGE`. +pub(crate) fn validate_number(bytes: &[u8]) -> Result<(), qjson_err> { let mut i = 0; // optional minus @@ -23,7 +23,7 @@ pub(crate) fn validate_number(bytes: &[u8]) -> Result<(), qjd_err> { i += 1; } } - _ => return Err(qjd_err::QJD_INVALID_NUMBER), + _ => return Err(qjson_err::QJSON_INVALID_NUMBER), } // optional frac: "." 1*digit @@ -34,7 +34,7 @@ pub(crate) fn validate_number(bytes: &[u8]) -> Result<(), qjd_err> { if !c.is_ascii_digit() { break; } i += 1; } - if i == frac_start { return Err(qjd_err::QJD_INVALID_NUMBER); } + if i == frac_start { return Err(qjson_err::QJSON_INVALID_NUMBER); } } // optional exp: ("e"|"E") ["+"|"-"] 1*digit @@ -46,10 +46,10 @@ pub(crate) fn validate_number(bytes: &[u8]) -> Result<(), qjd_err> { if !c.is_ascii_digit() { break; } i += 1; } - if i == exp_start { return Err(qjd_err::QJD_INVALID_NUMBER); } + if i == exp_start { return Err(qjson_err::QJSON_INVALID_NUMBER); } } - if i != bytes.len() { return Err(qjd_err::QJD_INVALID_NUMBER); } + if i != bytes.len() { return Err(qjson_err::QJSON_INVALID_NUMBER); } Ok(()) } diff --git a/src/validate/strings/avx2.rs b/src/validate/strings/avx2.rs index 7823d8c..8391a93 100644 --- a/src/validate/strings/avx2.rs +++ b/src/validate/strings/avx2.rs @@ -12,19 +12,19 @@ //! The fast-path payoff comes from cleanly skipping long ASCII prefixes; //! the scalar tail handles correctness without needing SIMD escape logic. -use crate::error::qjd_err; +use crate::error::qjson_err; use core::arch::x86_64::*; use super::scalar::validate_span_scalar; /// Validate `span` using AVX2 to bulk-skip pure-ASCII 32-byte chunks. -pub(crate) fn validate_span_avx2(span: &[u8]) -> Result<(), qjd_err> { +pub(crate) fn validate_span_avx2(span: &[u8]) -> Result<(), qjson_err> { // SAFETY: dispatcher has verified the AVX2 feature is present. unsafe { validate_span_avx2_impl(span) } } #[target_feature(enable = "avx2")] -unsafe fn validate_span_avx2_impl(span: &[u8]) -> Result<(), qjd_err> { +unsafe fn validate_span_avx2_impl(span: &[u8]) -> Result<(), qjson_err> { let mut i: usize = 0; let n = span.len(); diff --git a/src/validate/strings/mod.rs b/src/validate/strings/mod.rs index ab10090..18c160b 100644 --- a/src/validate/strings/mod.rs +++ b/src/validate/strings/mod.rs @@ -17,16 +17,16 @@ mod avx2; #[cfg(target_arch = "aarch64")] mod neon; -use crate::error::qjd_err; +use crate::error::qjson_err; use once_cell::sync::OnceCell; -type ValidateFn = fn(&[u8]) -> Result<(), qjd_err>; +type ValidateFn = fn(&[u8]) -> Result<(), qjson_err>; static VALIDATE_FN: OnceCell = OnceCell::new(); /// Verify that the raw span (excluding surrounding quotes) contains no /// unescaped control characters (0x00..=0x1F), every backslash escape is /// RFC 8259 §7 compliant, and the byte sequence is valid UTF-8 per RFC 3629. -pub(crate) fn validate_string_span(span: &[u8]) -> Result<(), qjd_err> { +pub(crate) fn validate_string_span(span: &[u8]) -> Result<(), qjson_err> { let f = *VALIDATE_FN.get_or_init(|| { #[cfg(all(target_arch = "x86_64", feature = "avx2"))] { @@ -58,11 +58,11 @@ mod tests { #[test] fn ascii_ok() { assert!(validate_string_span(b"hello").is_ok()); } #[test] fn utf8_ok() { assert!(validate_string_span("中文".as_bytes()).is_ok()); } #[test] fn escapes_ok() { assert!(validate_string_span(b"a\\nb\\u00e9").is_ok()); } - #[test] fn tab_raw_bad() { assert_eq!(validate_string_span(b"a\tb").unwrap_err(), qjd_err::QJD_INVALID_STRING); } - #[test] fn null_raw_bad() { assert_eq!(validate_string_span(b"a\x00b").unwrap_err(), qjd_err::QJD_INVALID_STRING); } - #[test] fn newline_raw_bad() { assert_eq!(validate_string_span(b"a\nb").unwrap_err(), qjd_err::QJD_INVALID_STRING); } + #[test] fn tab_raw_bad() { assert_eq!(validate_string_span(b"a\tb").unwrap_err(), qjson_err::QJSON_INVALID_STRING); } + #[test] fn null_raw_bad() { assert_eq!(validate_string_span(b"a\x00b").unwrap_err(), qjson_err::QJSON_INVALID_STRING); } + #[test] fn newline_raw_bad() { assert_eq!(validate_string_span(b"a\nb").unwrap_err(), qjson_err::QJSON_INVALID_STRING); } #[test] fn del_0x7f_ok() { assert!(validate_string_span(b"a\x7fb").is_ok()); } // RFC 8259 does NOT forbid 0x7F - #[test] fn invalid_utf8_bad() { assert_eq!(validate_string_span(&[0xC0, 0xC0]).unwrap_err(), qjd_err::QJD_INVALID_UTF8); } + #[test] fn invalid_utf8_bad() { assert_eq!(validate_string_span(&[0xC0, 0xC0]).unwrap_err(), qjson_err::QJSON_INVALID_UTF8); } // ── Single-pass / SIMD edge cases ──────────────────────────────────── @@ -83,7 +83,7 @@ mod tests { // Long ASCII run skipped by SIMD, then a control byte in the tail. let mut s = vec![b'x'; 200]; s.push(b'\t'); - assert_eq!(validate_string_span(&s).unwrap_err(), qjd_err::QJD_INVALID_STRING); + assert_eq!(validate_string_span(&s).unwrap_err(), qjson_err::QJSON_INVALID_STRING); } #[test] @@ -112,59 +112,59 @@ mod tests { let mut s = vec![b'x'; 31]; s.push(b'\\'); s.push(b'q'); - assert_eq!(validate_string_span(&s).unwrap_err(), qjd_err::QJD_INVALID_STRING); + assert_eq!(validate_string_span(&s).unwrap_err(), qjson_err::QJSON_INVALID_STRING); } #[test] fn truncated_u_escape_at_end() { // `\uXX` with only 2 hex digits — RFC requires exactly 4. - assert_eq!(validate_string_span(b"\\uAB").unwrap_err(), qjd_err::QJD_INVALID_STRING); - assert_eq!(validate_string_span(b"\\uABC").unwrap_err(), qjd_err::QJD_INVALID_STRING); + assert_eq!(validate_string_span(b"\\uAB").unwrap_err(), qjson_err::QJSON_INVALID_STRING); + assert_eq!(validate_string_span(b"\\uABC").unwrap_err(), qjson_err::QJSON_INVALID_STRING); // Bare `\u` at end. - assert_eq!(validate_string_span(b"\\u").unwrap_err(), qjd_err::QJD_INVALID_STRING); + assert_eq!(validate_string_span(b"\\u").unwrap_err(), qjson_err::QJSON_INVALID_STRING); } #[test] fn dangling_backslash_at_end() { - assert_eq!(validate_string_span(b"abc\\").unwrap_err(), qjd_err::QJD_INVALID_STRING); + assert_eq!(validate_string_span(b"abc\\").unwrap_err(), qjson_err::QJSON_INVALID_STRING); } #[test] fn unknown_escape_introducer() { // `\a`, `\q`, etc. are not valid RFC 8259 escapes. - assert_eq!(validate_string_span(b"\\a").unwrap_err(), qjd_err::QJD_INVALID_STRING); - assert_eq!(validate_string_span(b"\\q").unwrap_err(), qjd_err::QJD_INVALID_STRING); - assert_eq!(validate_string_span(b"\\x41").unwrap_err(), qjd_err::QJD_INVALID_STRING); + assert_eq!(validate_string_span(b"\\a").unwrap_err(), qjson_err::QJSON_INVALID_STRING); + assert_eq!(validate_string_span(b"\\q").unwrap_err(), qjson_err::QJSON_INVALID_STRING); + assert_eq!(validate_string_span(b"\\x41").unwrap_err(), qjson_err::QJSON_INVALID_STRING); } #[test] fn u_escape_non_hex_bad() { - assert_eq!(validate_string_span(b"\\u00ZZ").unwrap_err(), qjd_err::QJD_INVALID_STRING); - assert_eq!(validate_string_span(b"\\uGHIJ").unwrap_err(), qjd_err::QJD_INVALID_STRING); + assert_eq!(validate_string_span(b"\\u00ZZ").unwrap_err(), qjson_err::QJSON_INVALID_STRING); + assert_eq!(validate_string_span(b"\\uGHIJ").unwrap_err(), qjson_err::QJSON_INVALID_STRING); } #[test] fn overlong_utf8_rejected() { // C0 80 would encode U+0000 in 2 bytes (overlong) — RFC 3629 forbids. - assert_eq!(validate_string_span(&[0xC0, 0x80]).unwrap_err(), qjd_err::QJD_INVALID_UTF8); + assert_eq!(validate_string_span(&[0xC0, 0x80]).unwrap_err(), qjson_err::QJSON_INVALID_UTF8); // E0 80 80 would encode U+0000 in 3 bytes (overlong). - assert_eq!(validate_string_span(&[0xE0, 0x80, 0x80]).unwrap_err(), qjd_err::QJD_INVALID_UTF8); + assert_eq!(validate_string_span(&[0xE0, 0x80, 0x80]).unwrap_err(), qjson_err::QJSON_INVALID_UTF8); // F0 80 80 80 would encode U+0000 in 4 bytes (overlong). - assert_eq!(validate_string_span(&[0xF0, 0x80, 0x80, 0x80]).unwrap_err(), qjd_err::QJD_INVALID_UTF8); + assert_eq!(validate_string_span(&[0xF0, 0x80, 0x80, 0x80]).unwrap_err(), qjson_err::QJSON_INVALID_UTF8); } #[test] fn surrogate_in_utf8_rejected() { // ED A0 80 = U+D800, the start of the high-surrogate range. - assert_eq!(validate_string_span(&[0xED, 0xA0, 0x80]).unwrap_err(), qjd_err::QJD_INVALID_UTF8); + assert_eq!(validate_string_span(&[0xED, 0xA0, 0x80]).unwrap_err(), qjson_err::QJSON_INVALID_UTF8); // ED BF BF = U+DFFF, the end of the low-surrogate range. - assert_eq!(validate_string_span(&[0xED, 0xBF, 0xBF]).unwrap_err(), qjd_err::QJD_INVALID_UTF8); + assert_eq!(validate_string_span(&[0xED, 0xBF, 0xBF]).unwrap_err(), qjson_err::QJSON_INVALID_UTF8); } #[test] fn lone_continuation_byte_rejected() { - assert_eq!(validate_string_span(&[0x80]).unwrap_err(), qjd_err::QJD_INVALID_UTF8); - assert_eq!(validate_string_span(&[b'a', 0xBF, b'b']).unwrap_err(), qjd_err::QJD_INVALID_UTF8); + assert_eq!(validate_string_span(&[0x80]).unwrap_err(), qjson_err::QJSON_INVALID_UTF8); + assert_eq!(validate_string_span(&[b'a', 0xBF, b'b']).unwrap_err(), qjson_err::QJSON_INVALID_UTF8); } #[test] @@ -176,17 +176,17 @@ mod tests { #[test] fn truncated_utf8_sequence_rejected() { // 2-byte lead with no continuation. - assert_eq!(validate_string_span(&[0xC3]).unwrap_err(), qjd_err::QJD_INVALID_UTF8); + assert_eq!(validate_string_span(&[0xC3]).unwrap_err(), qjson_err::QJSON_INVALID_UTF8); // 3-byte lead with only one continuation. - assert_eq!(validate_string_span(&[0xE4, 0xB8]).unwrap_err(), qjd_err::QJD_INVALID_UTF8); + assert_eq!(validate_string_span(&[0xE4, 0xB8]).unwrap_err(), qjson_err::QJSON_INVALID_UTF8); // 4-byte lead with only two continuations. - assert_eq!(validate_string_span(&[0xF0, 0x9F, 0x98]).unwrap_err(), qjd_err::QJD_INVALID_UTF8); + assert_eq!(validate_string_span(&[0xF0, 0x9F, 0x98]).unwrap_err(), qjson_err::QJSON_INVALID_UTF8); } #[test] fn utf8_out_of_range_rejected() { // F5..FF are not valid lead bytes (would encode > U+10FFFF). - assert_eq!(validate_string_span(&[0xF5, 0x80, 0x80, 0x80]).unwrap_err(), qjd_err::QJD_INVALID_UTF8); - assert_eq!(validate_string_span(&[0xFF]).unwrap_err(), qjd_err::QJD_INVALID_UTF8); + assert_eq!(validate_string_span(&[0xF5, 0x80, 0x80, 0x80]).unwrap_err(), qjson_err::QJSON_INVALID_UTF8); + assert_eq!(validate_string_span(&[0xFF]).unwrap_err(), qjson_err::QJSON_INVALID_UTF8); } } diff --git a/src/validate/strings/neon.rs b/src/validate/strings/neon.rs index 34d887e..75822a8 100644 --- a/src/validate/strings/neon.rs +++ b/src/validate/strings/neon.rs @@ -8,20 +8,20 @@ //! can be skipped entirely. The first non-zero chunk hands off to the //! scalar state machine, which handles correctness for the remainder. -use crate::error::qjd_err; +use crate::error::qjson_err; use core::arch::aarch64::*; use super::scalar::validate_span_scalar; /// Validate `span` using NEON to bulk-skip pure-ASCII 16-byte chunks. -pub(crate) fn validate_span_neon(span: &[u8]) -> Result<(), qjd_err> { +pub(crate) fn validate_span_neon(span: &[u8]) -> Result<(), qjson_err> { // SAFETY: aarch64 NEON is always available on aarch64 (it is part of // the AArch64 base ISA), so no runtime feature check is required. unsafe { validate_span_neon_impl(span) } } #[target_feature(enable = "neon")] -unsafe fn validate_span_neon_impl(span: &[u8]) -> Result<(), qjd_err> { +unsafe fn validate_span_neon_impl(span: &[u8]) -> Result<(), qjson_err> { let mut i: usize = 0; let n = span.len(); diff --git a/src/validate/strings/scalar.rs b/src/validate/strings/scalar.rs index 7784679..c821709 100644 --- a/src/validate/strings/scalar.rs +++ b/src/validate/strings/scalar.rs @@ -17,12 +17,12 @@ //! pins down which wins on mixed input, so the position-ordered choice here //! is the natural single-pass behavior. -use crate::error::qjd_err; +use crate::error::qjson_err; /// Validate `span` byte-by-byte. The caller passes the unescaped string /// interior (between the JSON `"…"` quotes) — `\` therefore introduces an /// RFC 8259 escape sequence, not a literal backslash byte. -pub(crate) fn validate_span_scalar(span: &[u8]) -> Result<(), qjd_err> { +pub(crate) fn validate_span_scalar(span: &[u8]) -> Result<(), qjson_err> { let mut i: usize = 0; let n = span.len(); while i < n { @@ -31,7 +31,7 @@ pub(crate) fn validate_span_scalar(span: &[u8]) -> Result<(), qjd_err> { // Fast path: plain ASCII non-escape non-control. if b < 0x80 { if b < 0x20 { - return Err(qjd_err::QJD_INVALID_STRING); + return Err(qjson_err::QJSON_INVALID_STRING); } if b == b'\\' { i = validate_escape(span, i + 1)?; @@ -50,10 +50,10 @@ pub(crate) fn validate_span_scalar(span: &[u8]) -> Result<(), qjd_err> { /// At entry `i` points to the byte AFTER the `\`. Returns the index of the /// next byte to validate (i.e. one past the last consumed escape byte). #[inline] -fn validate_escape(span: &[u8], i: usize) -> Result { +fn validate_escape(span: &[u8], i: usize) -> Result { if i >= span.len() { // Dangling `\` at end of span. - return Err(qjd_err::QJD_INVALID_STRING); + return Err(qjson_err::QJSON_INVALID_STRING); } match span[i] { b'"' | b'\\' | b'/' | b'b' | b'f' | b'n' | b'r' | b't' => Ok(i + 1), @@ -62,16 +62,16 @@ fn validate_escape(span: &[u8], i: usize) -> Result { let hex_start = i + 1; let hex_end = hex_start + 4; if hex_end > span.len() { - return Err(qjd_err::QJD_INVALID_STRING); + return Err(qjson_err::QJSON_INVALID_STRING); } for &h in &span[hex_start..hex_end] { if !h.is_ascii_hexdigit() { - return Err(qjd_err::QJD_INVALID_STRING); + return Err(qjson_err::QJSON_INVALID_STRING); } } Ok(hex_end) } - _ => Err(qjd_err::QJD_INVALID_STRING), + _ => Err(qjson_err::QJSON_INVALID_STRING), } } @@ -80,18 +80,18 @@ fn validate_escape(span: &[u8], i: usize) -> Result { /// encodings and UTF-16 surrogates U+D800..=U+DFFF). Returns the index one /// past the last byte of the sequence. #[inline] -fn validate_utf8_sequence(span: &[u8], i: usize) -> Result { +fn validate_utf8_sequence(span: &[u8], i: usize) -> Result { let lead = span[i]; let n = span.len(); // 2-byte: 110xxxxx 10xxxxxx, lead in C2..=DF (C0/C1 are overlong). if (0xC2..=0xDF).contains(&lead) { if i + 1 >= n { - return Err(qjd_err::QJD_INVALID_UTF8); + return Err(qjson_err::QJSON_INVALID_UTF8); } let b1 = span[i + 1]; if !(0x80..=0xBF).contains(&b1) { - return Err(qjd_err::QJD_INVALID_UTF8); + return Err(qjson_err::QJSON_INVALID_UTF8); } return Ok(i + 2); } @@ -101,7 +101,7 @@ fn validate_utf8_sequence(span: &[u8], i: usize) -> Result { // ED second must be 80..9F (else surrogate U+D800..=DFFF). if (0xE0..=0xEF).contains(&lead) { if i + 2 >= n { - return Err(qjd_err::QJD_INVALID_UTF8); + return Err(qjson_err::QJSON_INVALID_UTF8); } let b1 = span[i + 1]; let b2 = span[i + 2]; @@ -114,10 +114,10 @@ fn validate_utf8_sequence(span: &[u8], i: usize) -> Result { _ => 0xBF, }; if b1 < b1_lo || b1 > b1_hi { - return Err(qjd_err::QJD_INVALID_UTF8); + return Err(qjson_err::QJSON_INVALID_UTF8); } if !(0x80..=0xBF).contains(&b2) { - return Err(qjd_err::QJD_INVALID_UTF8); + return Err(qjson_err::QJSON_INVALID_UTF8); } return Ok(i + 3); } @@ -127,7 +127,7 @@ fn validate_utf8_sequence(span: &[u8], i: usize) -> Result { // F4 second must be 80..8F (else > U+10FFFF). if (0xF0..=0xF4).contains(&lead) { if i + 3 >= n { - return Err(qjd_err::QJD_INVALID_UTF8); + return Err(qjson_err::QJSON_INVALID_UTF8); } let b1 = span[i + 1]; let b2 = span[i + 2]; @@ -141,18 +141,18 @@ fn validate_utf8_sequence(span: &[u8], i: usize) -> Result { _ => 0xBF, }; if b1 < b1_lo || b1 > b1_hi { - return Err(qjd_err::QJD_INVALID_UTF8); + return Err(qjson_err::QJSON_INVALID_UTF8); } if !(0x80..=0xBF).contains(&b2) { - return Err(qjd_err::QJD_INVALID_UTF8); + return Err(qjson_err::QJSON_INVALID_UTF8); } if !(0x80..=0xBF).contains(&b3) { - return Err(qjd_err::QJD_INVALID_UTF8); + return Err(qjson_err::QJSON_INVALID_UTF8); } return Ok(i + 4); } // C0, C1 (overlong 2-byte lead), F5..FF (out of range), or a bare // continuation byte (80..BF with no lead) — all invalid. - Err(qjd_err::QJD_INVALID_UTF8) + Err(qjson_err::QJSON_INVALID_UTF8) } diff --git a/tests/ffi_cursor.rs b/tests/ffi_cursor.rs index 722bc42..29d21fb 100644 --- a/tests/ffi_cursor.rs +++ b/tests/ffi_cursor.rs @@ -1,9 +1,9 @@ use std::os::raw::c_int; -use quickdecode::ffi::*; +use qjson::ffi::*; -fn parse(s: &[u8]) -> *mut qjd_doc { +fn parse(s: &[u8]) -> *mut qjson_doc { let mut err: c_int = -1; - let d = unsafe { qjd_parse(s.as_ptr(), s.len(), &mut err) }; + let d = unsafe { qjson_parse(s.as_ptr(), s.len(), &mut err) }; assert!(!d.is_null()); d } @@ -11,99 +11,102 @@ fn parse(s: &[u8]) -> *mut qjd_doc { #[test] fn open_object_then_get_field() { let d = parse(b"{\"body\":{\"model\":\"gpt\",\"temperature\":0.5}}"); - let mut c = std::mem::MaybeUninit::::uninit(); + let mut c = std::mem::MaybeUninit::::uninit(); let p = b"body"; - let rc = unsafe { qjd_open(d, p.as_ptr() as *const i8, p.len(), c.as_mut_ptr()) }; + let rc = unsafe { qjson_open(d, p.as_ptr() as *const i8, p.len(), c.as_mut_ptr()) }; assert_eq!(rc, 0); let c = unsafe { c.assume_init() }; let mut pp: *const u8 = std::ptr::null(); let mut nn: usize = 0; let k = b"model"; - let rc = unsafe { qjd_cursor_get_str(&c, k.as_ptr() as *const i8, k.len(), &mut pp, &mut nn) }; + let rc = unsafe { qjson_cursor_get_str(&c, k.as_ptr() as *const i8, k.len(), &mut pp, &mut nn) }; assert_eq!(rc, 0); let s = unsafe { std::slice::from_raw_parts(pp, nn) }; assert_eq!(s, b"gpt"); let mut f: f64 = 0.0; let k = b"temperature"; - let rc = unsafe { qjd_cursor_get_f64(&c, k.as_ptr() as *const i8, k.len(), &mut f) }; + let rc = unsafe { qjson_cursor_get_f64(&c, k.as_ptr() as *const i8, k.len(), &mut f) }; assert_eq!(rc, 0); assert!((f - 0.5).abs() < 1e-12); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] fn cursor_index_array() { let d = parse(b"[\"a\",\"b\",\"c\"]"); - let mut c = std::mem::MaybeUninit::::uninit(); + let mut c = std::mem::MaybeUninit::::uninit(); let p = b""; - unsafe { qjd_open(d, p.as_ptr() as *const i8, 0, c.as_mut_ptr()) }; + let rc = unsafe { qjson_open(d, p.as_ptr() as *const i8, 0, c.as_mut_ptr()) }; + assert_eq!(rc, 0); let c = unsafe { c.assume_init() }; - let mut sub = std::mem::MaybeUninit::::uninit(); - let rc = unsafe { qjd_cursor_index(&c, 1, sub.as_mut_ptr()) }; + let mut sub = std::mem::MaybeUninit::::uninit(); + let rc = unsafe { qjson_cursor_index(&c, 1, sub.as_mut_ptr()) }; assert_eq!(rc, 0); let sub = unsafe { sub.assume_init() }; let mut pp: *const u8 = std::ptr::null(); let mut nn: usize = 0; let empty = b""; - let rc = unsafe { qjd_cursor_get_str(&sub, empty.as_ptr() as *const i8, 0, &mut pp, &mut nn) }; + let rc = unsafe { qjson_cursor_get_str(&sub, empty.as_ptr() as *const i8, 0, &mut pp, &mut nn) }; assert_eq!(rc, 0); assert_eq!(unsafe { std::slice::from_raw_parts(pp, nn) }, b"b"); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] fn cursor_field_with_dotted_key() { let d = parse(b"{\"a.b\":42}"); - let mut c = std::mem::MaybeUninit::::uninit(); + let mut c = std::mem::MaybeUninit::::uninit(); let p = b""; - unsafe { qjd_open(d, p.as_ptr() as *const i8, 0, c.as_mut_ptr()) }; + let rc = unsafe { qjson_open(d, p.as_ptr() as *const i8, 0, c.as_mut_ptr()) }; + assert_eq!(rc, 0); let c = unsafe { c.assume_init() }; - let mut sub = std::mem::MaybeUninit::::uninit(); + let mut sub = std::mem::MaybeUninit::::uninit(); let key = b"a.b"; - let rc = unsafe { qjd_cursor_field(&c, key.as_ptr() as *const i8, key.len(), sub.as_mut_ptr()) }; + let rc = unsafe { qjson_cursor_field(&c, key.as_ptr() as *const i8, key.len(), sub.as_mut_ptr()) }; assert_eq!(rc, 0); let sub = unsafe { sub.assume_init() }; let mut v: i64 = 0; let empty = b""; - let rc = unsafe { qjd_cursor_get_i64(&sub, empty.as_ptr() as *const i8, 0, &mut v) }; + let rc = unsafe { qjson_cursor_get_i64(&sub, empty.as_ptr() as *const i8, 0, &mut v) }; assert_eq!(rc, 0); assert_eq!(v, 42); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } // Regression: walk_children must visit the trailing scalar (last element has no // structural marker of its own — `indices[end]` is the parent closer). // Before the fix, `while i < end` stopped one step early and index 2 returned -// QJD_NOT_FOUND for a 3-element all-scalar array. +// QJSON_NOT_FOUND for a 3-element all-scalar array. #[test] fn walk_children_trailing_scalar_integer() { let d = parse(b"[10,20,30]"); - let mut c = std::mem::MaybeUninit::::uninit(); + let mut c = std::mem::MaybeUninit::::uninit(); let empty = b""; - unsafe { qjd_open(d, empty.as_ptr() as *const i8, 0, c.as_mut_ptr()) }; + let rc = unsafe { qjson_open(d, empty.as_ptr() as *const i8, 0, c.as_mut_ptr()) }; + assert_eq!(rc, 0); let c = unsafe { c.assume_init() }; // Index 2 is the trailing element `30`. - let mut sub = std::mem::MaybeUninit::::uninit(); - let rc = unsafe { qjd_cursor_index(&c, 2, sub.as_mut_ptr()) }; - assert_eq!(rc, 0, "qjd_cursor_index([2]) must succeed"); + let mut sub = std::mem::MaybeUninit::::uninit(); + let rc = unsafe { qjson_cursor_index(&c, 2, sub.as_mut_ptr()) }; + assert_eq!(rc, 0, "qjson_cursor_index([2]) must succeed"); let sub = unsafe { sub.assume_init() }; let mut v: i64 = 0; - let rc = unsafe { qjd_cursor_get_i64(&sub, empty.as_ptr() as *const i8, 0, &mut v) }; - assert_eq!(rc, 0, "qjd_cursor_get_i64 on trailing element must succeed"); + let rc = unsafe { qjson_cursor_get_i64(&sub, empty.as_ptr() as *const i8, 0, &mut v) }; + assert_eq!(rc, 0, "qjson_cursor_get_i64 on trailing element must succeed"); assert_eq!(v, 30); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } // Regression: trailing scalar with non-numeric type — ensures walk_children @@ -111,21 +114,22 @@ fn walk_children_trailing_scalar_integer() { #[test] fn walk_children_trailing_scalar_bool() { let d = parse(b"[1,\"x\",true]"); - let mut c = std::mem::MaybeUninit::::uninit(); + let mut c = std::mem::MaybeUninit::::uninit(); let empty = b""; - unsafe { qjd_open(d, empty.as_ptr() as *const i8, 0, c.as_mut_ptr()) }; + let rc = unsafe { qjson_open(d, empty.as_ptr() as *const i8, 0, c.as_mut_ptr()) }; + assert_eq!(rc, 0); let c = unsafe { c.assume_init() }; // Index 2 is the trailing element `true`. - let mut sub = std::mem::MaybeUninit::::uninit(); - let rc = unsafe { qjd_cursor_index(&c, 2, sub.as_mut_ptr()) }; - assert_eq!(rc, 0, "qjd_cursor_index([2]) must succeed"); + let mut sub = std::mem::MaybeUninit::::uninit(); + let rc = unsafe { qjson_cursor_index(&c, 2, sub.as_mut_ptr()) }; + assert_eq!(rc, 0, "qjson_cursor_index([2]) must succeed"); let sub = unsafe { sub.assume_init() }; let mut b: c_int = -1; - let rc = unsafe { qjd_cursor_get_bool(&sub, empty.as_ptr() as *const i8, 0, &mut b) }; - assert_eq!(rc, 0, "qjd_cursor_get_bool on trailing `true` must succeed"); + let rc = unsafe { qjson_cursor_get_bool(&sub, empty.as_ptr() as *const i8, 0, &mut b) }; + assert_eq!(rc, 0, "qjson_cursor_get_bool on trailing `true` must succeed"); assert_eq!(b, 1); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } diff --git a/tests/ffi_cursor_bytes.rs b/tests/ffi_cursor_bytes.rs index c7d4821..b47fda4 100644 --- a/tests/ffi_cursor_bytes.rs +++ b/tests/ffi_cursor_bytes.rs @@ -1,17 +1,17 @@ use std::os::raw::c_int; use std::ptr; -use quickdecode::error::qjd_err; -use quickdecode::ffi::{ - qjd_cursor, qjd_cursor_bytes, qjd_cursor_field, qjd_doc, qjd_free, qjd_open, qjd_parse, +use qjson::error::qjson_err; +use qjson::ffi::{ + qjson_cursor, qjson_cursor_bytes, qjson_cursor_field, qjson_doc, qjson_free, qjson_open, qjson_parse, }; -unsafe fn open_root(json: &[u8]) -> (*mut qjd_doc, qjd_cursor) { +unsafe fn open_root(json: &[u8]) -> (*mut qjson_doc, qjson_cursor) { let mut err: c_int = -1; - let doc = qjd_parse(json.as_ptr(), json.len(), &mut err); + let doc = qjson_parse(json.as_ptr(), json.len(), &mut err); assert!(!doc.is_null()); - let mut cur: qjd_cursor = std::mem::zeroed(); - let rc = qjd_open(doc, ptr::null(), 0, &mut cur); + let mut cur: qjson_cursor = std::mem::zeroed(); + let rc = qjson_open(doc, ptr::null(), 0, &mut cur); assert_eq!(rc, 0); (doc, cur) } @@ -23,10 +23,10 @@ fn bytes_of_root_object_covers_full_json() { let (doc, cur) = open_root(json); let mut bs: usize = 0; let mut be: usize = 0; - let rc = qjd_cursor_bytes(&cur, &mut bs, &mut be); + let rc = qjson_cursor_bytes(&cur, &mut bs, &mut be); assert_eq!(rc, 0); assert_eq!(&json[bs..be], json.as_ref()); - qjd_free(doc); + qjson_free(doc); } } @@ -35,15 +35,15 @@ fn bytes_of_string_value_is_quoted_span() { let json = br#"{"k":"hello"}"#; unsafe { let (doc, root) = open_root(json); - let mut child: qjd_cursor = std::mem::zeroed(); - let rc = qjd_cursor_field(&root, b"k".as_ptr() as *const i8, 1, &mut child); + let mut child: qjson_cursor = std::mem::zeroed(); + let rc = qjson_cursor_field(&root, b"k".as_ptr() as *const i8, 1, &mut child); assert_eq!(rc, 0); let mut bs: usize = 0; let mut be: usize = 0; - let rc = qjd_cursor_bytes(&child, &mut bs, &mut be); + let rc = qjson_cursor_bytes(&child, &mut bs, &mut be); assert_eq!(rc, 0); assert_eq!(&json[bs..be], br#""hello""#); - qjd_free(doc); + qjson_free(doc); } } @@ -52,15 +52,15 @@ fn bytes_of_number_value_strips_separators() { let json = br#"{"k": 42 ,"x":1}"#; unsafe { let (doc, root) = open_root(json); - let mut child: qjd_cursor = std::mem::zeroed(); - let rc = qjd_cursor_field(&root, b"k".as_ptr() as *const i8, 1, &mut child); + let mut child: qjson_cursor = std::mem::zeroed(); + let rc = qjson_cursor_field(&root, b"k".as_ptr() as *const i8, 1, &mut child); assert_eq!(rc, 0); let mut bs: usize = 0; let mut be: usize = 0; - let rc = qjd_cursor_bytes(&child, &mut bs, &mut be); + let rc = qjson_cursor_bytes(&child, &mut bs, &mut be); assert_eq!(rc, 0); assert_eq!(&json[bs..be], b"42"); - qjd_free(doc); + qjson_free(doc); } } @@ -69,9 +69,9 @@ fn bytes_with_null_out_pointer_returns_invalid_arg() { let json = br#"{"a":1}"#; unsafe { let (doc, root) = open_root(json); - let rc = qjd_cursor_bytes(&root, ptr::null_mut(), ptr::null_mut()); - assert_eq!(rc, qjd_err::QJD_INVALID_ARG as c_int); - qjd_free(doc); + let rc = qjson_cursor_bytes(&root, ptr::null_mut(), ptr::null_mut()); + assert_eq!(rc, qjson_err::QJSON_INVALID_ARG as c_int); + qjson_free(doc); } } @@ -82,9 +82,9 @@ fn bytes_of_root_array_covers_full_json() { let (doc, cur) = open_root(json); let mut bs: usize = 0; let mut be: usize = 0; - let rc = qjd_cursor_bytes(&cur, &mut bs, &mut be); + let rc = qjson_cursor_bytes(&cur, &mut bs, &mut be); assert_eq!(rc, 0); assert_eq!(&json[bs..be], json.as_ref()); - qjd_free(doc); + qjson_free(doc); } } diff --git a/tests/ffi_numbers.rs b/tests/ffi_numbers.rs index eb65394..84eac57 100644 --- a/tests/ffi_numbers.rs +++ b/tests/ffi_numbers.rs @@ -1,9 +1,9 @@ use std::os::raw::c_int; -use quickdecode::ffi::*; +use qjson::ffi::*; -fn parse(s: &[u8]) -> *mut qjd_doc { +fn parse(s: &[u8]) -> *mut qjson_doc { let mut err: c_int = -1; - let d = unsafe { qjd_parse(s.as_ptr(), s.len(), &mut err) }; + let d = unsafe { qjson_parse(s.as_ptr(), s.len(), &mut err) }; assert!(!d.is_null()); d } @@ -13,10 +13,10 @@ fn get_i64_basic() { let d = parse(b"{\"a\":42}"); let mut v: i64 = 0; let p = b"a"; - let rc = unsafe { qjd_get_i64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + let rc = unsafe { qjson_get_i64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; assert_eq!(rc, 0); assert_eq!(v, 42); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] @@ -24,9 +24,10 @@ fn get_i64_negative() { let d = parse(b"{\"a\":-7}"); let mut v: i64 = 0; let p = b"a"; - unsafe { qjd_get_i64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + let rc = unsafe { qjson_get_i64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + assert_eq!(rc, 0); assert_eq!(v, -7); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] @@ -34,9 +35,9 @@ fn get_i64_overflow() { let d = parse(b"{\"a\":99999999999999999999}"); let mut v: i64 = 0; let p = b"a"; - let rc = unsafe { qjd_get_i64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + let rc = unsafe { qjson_get_i64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; assert_eq!(rc, 4); // OUT_OF_RANGE - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] @@ -44,9 +45,10 @@ fn get_f64_basic() { let d = parse(b"{\"a\":1.7}"); let mut v: f64 = 0.0; let p = b"a"; - unsafe { qjd_get_f64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + let rc = unsafe { qjson_get_f64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + assert_eq!(rc, 0); assert!((v - 1.7).abs() < 1e-12); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] @@ -54,12 +56,14 @@ fn get_bool() { let d = parse(b"{\"a\":true,\"b\":false}"); let mut v: c_int = -1; let p = b"a"; - unsafe { qjd_get_bool(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + let rc = unsafe { qjson_get_bool(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + assert_eq!(rc, 0); assert_ne!(v, 0); let p = b"b"; - unsafe { qjd_get_bool(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + let rc = unsafe { qjson_get_bool(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + assert_eq!(rc, 0); assert_eq!(v, 0); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] @@ -68,14 +72,14 @@ fn get_i64_max_and_min() { let d = parse(json.as_bytes()); let mut v: i64 = 0; let p = b"hi"; - let rc = unsafe { qjd_get_i64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + let rc = unsafe { qjson_get_i64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; assert_eq!(rc, 0); assert_eq!(v, i64::MAX); let p = b"lo"; - let rc = unsafe { qjd_get_i64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + let rc = unsafe { qjson_get_i64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; assert_eq!(rc, 0); assert_eq!(v, i64::MIN); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] @@ -84,9 +88,9 @@ fn get_i64_just_over_max_overflows() { let d = parse(b"{\"a\":9223372036854775808}"); let mut v: i64 = 0; let p = b"a"; - let rc = unsafe { qjd_get_i64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + let rc = unsafe { qjson_get_i64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; assert_eq!(rc, 4); // OUT_OF_RANGE - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] @@ -94,10 +98,10 @@ fn get_f64_large_magnitude() { let d = parse(b"{\"a\":1.7e308}"); let mut v: f64 = 0.0; let p = b"a"; - let rc = unsafe { qjd_get_f64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + let rc = unsafe { qjson_get_f64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; assert_eq!(rc, 0); assert!(v > 1.0e308 && v < f64::INFINITY); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] @@ -105,12 +109,14 @@ fn get_f64_negative_zero_and_exponent() { let d = parse(b"{\"a\":-0.0,\"b\":1e-300}"); let mut v: f64 = 1.0; let p = b"a"; - unsafe { qjd_get_f64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + let rc = unsafe { qjson_get_f64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + assert_eq!(rc, 0); assert_eq!(v, 0.0); let p = b"b"; - unsafe { qjd_get_f64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + let rc = unsafe { qjson_get_f64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + assert_eq!(rc, 0); assert!(v > 0.0 && v < 1e-200); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] @@ -118,7 +124,7 @@ fn get_i64_rejects_float_form() { let d = parse(b"{\"a\":1.5}"); let mut v: i64 = 0; let p = b"a"; - let rc = unsafe { qjd_get_i64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; + let rc = unsafe { qjson_get_i64(d, p.as_ptr() as *const i8, p.len(), &mut v) }; assert_ne!(rc, 0); // any error code is acceptable; not a valid i64 - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } diff --git a/tests/ffi_object_iter.rs b/tests/ffi_object_iter.rs index 622abf8..41ff276 100644 --- a/tests/ffi_object_iter.rs +++ b/tests/ffi_object_iter.rs @@ -1,24 +1,25 @@ use std::os::raw::c_int; use std::ptr; -use quickdecode::ffi::{ - qjd_cursor, qjd_cursor_object_entry_at, qjd_doc, qjd_free, qjd_open, qjd_parse, +use qjson::ffi::{ + qjson_cursor, qjson_cursor_object_entry_at, qjson_doc, qjson_free, qjson_open, qjson_parse, }; -unsafe fn open_root(json: &[u8]) -> (*mut qjd_doc, qjd_cursor) { +unsafe fn open_root(json: &[u8]) -> (*mut qjson_doc, qjson_cursor) { let mut err: c_int = -1; - let doc = qjd_parse(json.as_ptr(), json.len(), &mut err); + let doc = qjson_parse(json.as_ptr(), json.len(), &mut err); assert!(!doc.is_null()); - let mut cur: qjd_cursor = std::mem::zeroed(); - qjd_open(doc, ptr::null(), 0, &mut cur); + let mut cur: qjson_cursor = std::mem::zeroed(); + let rc = qjson_open(doc, ptr::null(), 0, &mut cur); + assert_eq!(rc, 0); (doc, cur) } -unsafe fn entry_at(root: &qjd_cursor, i: usize) -> (String, qjd_cursor) { +unsafe fn entry_at(root: &qjson_cursor, i: usize) -> (String, qjson_cursor) { let mut kp: *const u8 = ptr::null(); let mut kn: usize = 0; - let mut vc: qjd_cursor = std::mem::zeroed(); - let rc = qjd_cursor_object_entry_at(root, i, &mut kp, &mut kn, &mut vc); + let mut vc: qjson_cursor = std::mem::zeroed(); + let rc = qjson_cursor_object_entry_at(root, i, &mut kp, &mut kn, &mut vc); assert_eq!(rc, 0, "entry_at({}) failed with rc={}", i, rc); let key = std::slice::from_raw_parts(kp, kn); (String::from_utf8(key.to_vec()).unwrap(), vc) @@ -35,7 +36,7 @@ fn three_keys_in_order() { assert_eq!(k0, "a"); assert_eq!(k1, "b"); assert_eq!(k2, "c"); - qjd_free(doc); + qjson_free(doc); } } @@ -48,7 +49,7 @@ fn key_with_escape_decodes() { let (doc, root) = open_root(json); let (k0, _) = entry_at(&root, 0); assert_eq!(k0, "a\nb"); - qjd_free(doc); + qjson_free(doc); } } @@ -59,10 +60,10 @@ fn out_of_range_returns_not_found() { let (doc, root) = open_root(json); let mut kp: *const u8 = ptr::null(); let mut kn: usize = 0; - let mut vc: qjd_cursor = std::mem::zeroed(); - let rc = qjd_cursor_object_entry_at(&root, 5, &mut kp, &mut kn, &mut vc); - assert_eq!(rc, 2); // QJD_NOT_FOUND - qjd_free(doc); + let mut vc: qjson_cursor = std::mem::zeroed(); + let rc = qjson_cursor_object_entry_at(&root, 5, &mut kp, &mut kn, &mut vc); + assert_eq!(rc, 2); // QJSON_NOT_FOUND + qjson_free(doc); } } @@ -73,9 +74,9 @@ fn array_cursor_returns_type_mismatch() { let (doc, root) = open_root(json); let mut kp: *const u8 = ptr::null(); let mut kn: usize = 0; - let mut vc: qjd_cursor = std::mem::zeroed(); - let rc = qjd_cursor_object_entry_at(&root, 0, &mut kp, &mut kn, &mut vc); - assert_eq!(rc, 3); // QJD_TYPE_MISMATCH - qjd_free(doc); + let mut vc: qjson_cursor = std::mem::zeroed(); + let rc = qjson_cursor_object_entry_at(&root, 0, &mut kp, &mut kn, &mut vc); + assert_eq!(rc, 3); // QJSON_TYPE_MISMATCH + qjson_free(doc); } } diff --git a/tests/ffi_options_smoke.rs b/tests/ffi_options_smoke.rs index 83d942d..250b9f1 100644 --- a/tests/ffi_options_smoke.rs +++ b/tests/ffi_options_smoke.rs @@ -1,44 +1,44 @@ -//! Smoke test for qjd_parse_ex and qjd_options C ABI. +//! Smoke test for qjson_parse_ex and qjson_options C ABI. use std::os::raw::c_int; -use quickdecode::ffi::{qjd_doc, qjd_free, qjd_parse, qjd_parse_ex}; -use quickdecode::options::Options; +use qjson::ffi::{qjson_doc, qjson_free, qjson_parse, qjson_parse_ex}; +use qjson::options::Options; #[test] fn parse_ex_default_options_matches_parse() { let buf = b"{\"a\":1}"; let mut err: c_int = -1; - let d1: *mut qjd_doc = unsafe { qjd_parse(buf.as_ptr(), buf.len(), &mut err) }; + let d1: *mut qjson_doc = unsafe { qjson_parse(buf.as_ptr(), buf.len(), &mut err) }; assert!(!d1.is_null()); assert_eq!(err, 0); let opts = Options { mode: 0, max_depth: 0 }; let mut err2: c_int = -1; - let d2: *mut qjd_doc = unsafe { qjd_parse_ex(buf.as_ptr(), buf.len(), &opts, &mut err2) }; + let d2: *mut qjson_doc = unsafe { qjson_parse_ex(buf.as_ptr(), buf.len(), &opts, &mut err2) }; assert!(!d2.is_null()); assert_eq!(err2, 0); - unsafe { qjd_free(d1); qjd_free(d2); } + unsafe { qjson_free(d1); qjson_free(d2); } } #[test] fn parse_ex_null_opts_uses_defaults() { let buf = b"{}"; let mut err: c_int = -1; - let d: *mut qjd_doc = unsafe { - qjd_parse_ex(buf.as_ptr(), buf.len(), std::ptr::null(), &mut err) + let d: *mut qjson_doc = unsafe { + qjson_parse_ex(buf.as_ptr(), buf.len(), std::ptr::null(), &mut err) }; assert!(!d.is_null()); assert_eq!(err, 0); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] fn parse_ex_null_err_returns_null_on_bad_buf() { let opts = Options { mode: 0, max_depth: 0 }; - let d: *mut qjd_doc = unsafe { - qjd_parse_ex(std::ptr::null(), 0, &opts, std::ptr::null_mut()) + let d: *mut qjson_doc = unsafe { + qjson_parse_ex(std::ptr::null(), 0, &opts, std::ptr::null_mut()) }; assert!(d.is_null()); } diff --git a/tests/ffi_panic_safety.rs b/tests/ffi_panic_safety.rs index c715cd1..454dff6 100644 --- a/tests/ffi_panic_safety.rs +++ b/tests/ffi_panic_safety.rs @@ -1,9 +1,9 @@ #[cfg(feature = "test-panic")] #[test] fn panic_does_not_unwind_through_ffi() { - use quickdecode::ffi::qjd_test_panic; - let rc = unsafe { qjd_test_panic() }; - assert_eq!(rc, 8); // QJD_OOM + use qjson::ffi::qjson_test_panic; + let rc = unsafe { qjson_test_panic() }; + assert_eq!(rc, 8); // QJSON_OOM } #[cfg(not(feature = "test-panic"))] diff --git a/tests/ffi_smoke.rs b/tests/ffi_smoke.rs index 7fe5f3b..b89177b 100644 --- a/tests/ffi_smoke.rs +++ b/tests/ffi_smoke.rs @@ -1,44 +1,44 @@ use std::ffi::CStr; use std::os::raw::c_int; -use quickdecode::ffi::{qjd_doc, qjd_free, qjd_parse, qjd_strerror}; +use qjson::ffi::{qjson_doc, qjson_free, qjson_parse, qjson_strerror}; #[test] fn parse_and_free_roundtrip() { let json = b"{\"a\":1}"; let mut err: c_int = -1; - let doc: *mut qjd_doc = unsafe { qjd_parse(json.as_ptr(), json.len(), &mut err) }; + let doc: *mut qjson_doc = unsafe { qjson_parse(json.as_ptr(), json.len(), &mut err) }; assert!(!doc.is_null()); assert_eq!(err, 0); - unsafe { qjd_free(doc); } + unsafe { qjson_free(doc); } } #[test] fn parse_error_returns_null() { let bad = b"{"; let mut err: c_int = -1; - let doc = unsafe { qjd_parse(bad.as_ptr(), bad.len(), &mut err) }; + let doc = unsafe { qjson_parse(bad.as_ptr(), bad.len(), &mut err) }; assert!(doc.is_null()); - assert_eq!(err, 1); // QJD_PARSE_ERROR + assert_eq!(err, 1); // QJSON_PARSE_ERROR } #[test] fn parse_null_buffer_returns_invalid_arg() { let mut err: c_int = -1; - let doc = unsafe { qjd_parse(std::ptr::null(), 0, &mut err) }; + let doc = unsafe { qjson_parse(std::ptr::null(), 0, &mut err) }; assert!(doc.is_null()); - assert_eq!(err, 7); // QJD_INVALID_ARG + assert_eq!(err, 7); // QJSON_INVALID_ARG } #[test] fn free_null_is_safe() { - unsafe { qjd_free(std::ptr::null_mut()); } + unsafe { qjson_free(std::ptr::null_mut()); } } #[test] fn strerror_returns_non_empty() { for code in 0..=8 { - let p = unsafe { qjd_strerror(code) }; + let p = unsafe { qjson_strerror(code) }; assert!(!p.is_null()); let s = unsafe { CStr::from_ptr(p) }.to_str().unwrap(); assert!(!s.is_empty(), "code {}", code); diff --git a/tests/ffi_strings.rs b/tests/ffi_strings.rs index e68e055..a56ba45 100644 --- a/tests/ffi_strings.rs +++ b/tests/ffi_strings.rs @@ -1,9 +1,9 @@ use std::os::raw::c_int; -use quickdecode::ffi::*; +use qjson::ffi::*; -fn parse(s: &[u8]) -> *mut qjd_doc { +fn parse(s: &[u8]) -> *mut qjson_doc { let mut err: c_int = -1; - let d = unsafe { qjd_parse(s.as_ptr(), s.len(), &mut err) }; + let d = unsafe { qjson_parse(s.as_ptr(), s.len(), &mut err) }; assert!(!d.is_null()); d } @@ -14,11 +14,11 @@ fn get_str_simple() { let mut p: *const u8 = std::ptr::null(); let mut n: usize = 0; let path = b"a"; - let rc = unsafe { qjd_get_str(d, path.as_ptr() as *const i8, path.len(), &mut p, &mut n) }; + let rc = unsafe { qjson_get_str(d, path.as_ptr() as *const i8, path.len(), &mut p, &mut n) }; assert_eq!(rc, 0); let s = unsafe { std::slice::from_raw_parts(p, n) }; assert_eq!(s, b"hello"); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] @@ -27,11 +27,11 @@ fn get_str_with_escape() { let mut p: *const u8 = std::ptr::null(); let mut n: usize = 0; let path = b"a"; - let rc = unsafe { qjd_get_str(d, path.as_ptr() as *const i8, path.len(), &mut p, &mut n) }; + let rc = unsafe { qjson_get_str(d, path.as_ptr() as *const i8, path.len(), &mut p, &mut n) }; assert_eq!(rc, 0); let s = unsafe { std::slice::from_raw_parts(p, n) }; assert_eq!(s, b"he\nlo"); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] @@ -40,7 +40,7 @@ fn get_str_type_mismatch() { let mut p: *const u8 = std::ptr::null(); let mut n: usize = 0; let path = b"a"; - let rc = unsafe { qjd_get_str(d, path.as_ptr() as *const i8, path.len(), &mut p, &mut n) }; + let rc = unsafe { qjson_get_str(d, path.as_ptr() as *const i8, path.len(), &mut p, &mut n) }; assert_eq!(rc, 3); // TYPE_MISMATCH - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } diff --git a/tests/ffi_typeof.rs b/tests/ffi_typeof.rs index e5712d8..05a802b 100644 --- a/tests/ffi_typeof.rs +++ b/tests/ffi_typeof.rs @@ -1,9 +1,9 @@ use std::os::raw::c_int; -use quickdecode::ffi::*; +use qjson::ffi::*; -fn parse(s: &[u8]) -> *mut qjd_doc { +fn parse(s: &[u8]) -> *mut qjson_doc { let mut err: c_int = -1; - let d = unsafe { qjd_parse(s.as_ptr(), s.len(), &mut err) }; + let d = unsafe { qjson_parse(s.as_ptr(), s.len(), &mut err) }; assert!(!d.is_null()); d } @@ -13,10 +13,10 @@ fn typeof_string() { let d = parse(b"{\"a\":\"hi\"}"); let mut t: c_int = -1; let p = b"a"; - let rc = unsafe { qjd_typeof(d, p.as_ptr() as *const i8, p.len(), &mut t) }; + let rc = unsafe { qjson_typeof(d, p.as_ptr() as *const i8, p.len(), &mut t) }; assert_eq!(rc, 0); - assert_eq!(t, 3); // QJD_T_STR - unsafe { qjd_free(d) }; + assert_eq!(t, 3); // QJSON_T_STR + unsafe { qjson_free(d) }; } #[test] @@ -24,10 +24,10 @@ fn typeof_number() { let d = parse(b"{\"a\":42}"); let mut t: c_int = -1; let p = b"a"; - let rc = unsafe { qjd_typeof(d, p.as_ptr() as *const i8, p.len(), &mut t) }; + let rc = unsafe { qjson_typeof(d, p.as_ptr() as *const i8, p.len(), &mut t) }; assert_eq!(rc, 0); - assert_eq!(t, 2); // QJD_T_NUM - unsafe { qjd_free(d) }; + assert_eq!(t, 2); // QJSON_T_NUM + unsafe { qjson_free(d) }; } #[test] @@ -35,10 +35,10 @@ fn typeof_bool() { let d = parse(b"{\"a\":true}"); let mut t: c_int = -1; let p = b"a"; - let rc = unsafe { qjd_typeof(d, p.as_ptr() as *const i8, p.len(), &mut t) }; + let rc = unsafe { qjson_typeof(d, p.as_ptr() as *const i8, p.len(), &mut t) }; assert_eq!(rc, 0); assert_eq!(t, 1); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] @@ -46,10 +46,10 @@ fn typeof_null() { let d = parse(b"{\"a\":null}"); let mut t: c_int = -1; let p = b"a"; - let rc = unsafe { qjd_typeof(d, p.as_ptr() as *const i8, p.len(), &mut t) }; + let rc = unsafe { qjson_typeof(d, p.as_ptr() as *const i8, p.len(), &mut t) }; assert_eq!(rc, 0); assert_eq!(t, 0); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] @@ -57,10 +57,10 @@ fn is_null_true() { let d = parse(b"{\"a\":null}"); let mut b: c_int = -1; let p = b"a"; - let rc = unsafe { qjd_is_null(d, p.as_ptr() as *const i8, p.len(), &mut b) }; + let rc = unsafe { qjson_is_null(d, p.as_ptr() as *const i8, p.len(), &mut b) }; assert_eq!(rc, 0); assert_ne!(b, 0); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] @@ -68,10 +68,10 @@ fn len_object() { let d = parse(b"{\"a\":1,\"b\":2,\"c\":3}"); let mut n: usize = 0; let p = b""; - let rc = unsafe { qjd_len(d, p.as_ptr() as *const i8, p.len(), &mut n) }; + let rc = unsafe { qjson_len(d, p.as_ptr() as *const i8, p.len(), &mut n) }; assert_eq!(rc, 0); assert_eq!(n, 3); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] @@ -79,10 +79,10 @@ fn len_array() { let d = parse(b"[10,20,30,40]"); let mut n: usize = 0; let p = b""; - let rc = unsafe { qjd_len(d, p.as_ptr() as *const i8, p.len(), &mut n) }; + let rc = unsafe { qjson_len(d, p.as_ptr() as *const i8, p.len(), &mut n) }; assert_eq!(rc, 0); assert_eq!(n, 4); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] @@ -90,9 +90,9 @@ fn typeof_not_found() { let d = parse(b"{\"a\":1}"); let mut t: c_int = -1; let p = b"b"; - let rc = unsafe { qjd_typeof(d, p.as_ptr() as *const i8, p.len(), &mut t) }; + let rc = unsafe { qjson_typeof(d, p.as_ptr() as *const i8, p.len(), &mut t) }; assert_eq!(rc, 2); // NOT_FOUND - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] @@ -100,10 +100,10 @@ fn len_empty_object() { let d = parse(b"{}"); let mut n: usize = 0; let p = b""; - let rc = unsafe { qjd_len(d, p.as_ptr() as *const i8, p.len(), &mut n) }; + let rc = unsafe { qjson_len(d, p.as_ptr() as *const i8, p.len(), &mut n) }; assert_eq!(rc, 0); assert_eq!(n, 0); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] @@ -111,10 +111,10 @@ fn len_empty_array() { let d = parse(b"[]"); let mut n: usize = 0; let p = b""; - let rc = unsafe { qjd_len(d, p.as_ptr() as *const i8, p.len(), &mut n) }; + let rc = unsafe { qjson_len(d, p.as_ptr() as *const i8, p.len(), &mut n) }; assert_eq!(rc, 0); assert_eq!(n, 0); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] @@ -122,10 +122,10 @@ fn len_single_scalar_array() { let d = parse(b"[5]"); let mut n: usize = 0; let p = b""; - let rc = unsafe { qjd_len(d, p.as_ptr() as *const i8, p.len(), &mut n) }; + let rc = unsafe { qjson_len(d, p.as_ptr() as *const i8, p.len(), &mut n) }; assert_eq!(rc, 0); assert_eq!(n, 1); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } #[test] @@ -133,8 +133,8 @@ fn len_single_scalar_object() { let d = parse(b"{\"a\":1}"); let mut n: usize = 0; let p = b""; - let rc = unsafe { qjd_len(d, p.as_ptr() as *const i8, p.len(), &mut n) }; + let rc = unsafe { qjson_len(d, p.as_ptr() as *const i8, p.len(), &mut n) }; assert_eq!(rc, 0); assert_eq!(n, 1); - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } diff --git a/tests/ffi_wide_object.rs b/tests/ffi_wide_object.rs index 0dbbfc5..d2f082a 100644 --- a/tests/ffi_wide_object.rs +++ b/tests/ffi_wide_object.rs @@ -2,7 +2,7 @@ //! keys via the same cursor and confirm correctness. use std::os::raw::c_int; -use quickdecode::ffi::*; +use qjson::ffi::*; fn build_wide(n: usize) -> (String, Vec) { let mut s = String::from("{"); @@ -23,7 +23,7 @@ fn wide_object_5k_keys_all_resolvable() { let n = 5000; let (json, keys) = build_wide(n); let mut err: c_int = -1; - let d = unsafe { qjd_parse(json.as_ptr(), json.len(), &mut err) }; + let d = unsafe { qjson_parse(json.as_ptr(), json.len(), &mut err) }; assert!(!d.is_null()); // Hit a sparse sample, in non-sequential order, twice (second pass exercises @@ -32,14 +32,14 @@ fn wide_object_5k_keys_all_resolvable() { for &i in &samples { let mut v: i64 = -1; let k = keys[i].as_bytes(); - let rc = unsafe { qjd_get_i64(d, k.as_ptr() as *const i8, k.len(), &mut v) }; + let rc = unsafe { qjson_get_i64(d, k.as_ptr() as *const i8, k.len(), &mut v) }; assert_eq!(rc, 0, "miss on first pass for key {}", keys[i]); assert_eq!(v as usize, i * 2); } for &i in samples.iter().rev() { let mut v: i64 = -1; let k = keys[i].as_bytes(); - let rc = unsafe { qjd_get_i64(d, k.as_ptr() as *const i8, k.len(), &mut v) }; + let rc = unsafe { qjson_get_i64(d, k.as_ptr() as *const i8, k.len(), &mut v) }; assert_eq!(rc, 0, "miss on cache-hit pass for key {}", keys[i]); assert_eq!(v as usize, i * 2); } @@ -47,8 +47,8 @@ fn wide_object_5k_keys_all_resolvable() { // Unknown key still returns NOT_FOUND after the cache is populated. let bogus = b"definitely_not_a_key"; let mut v: i64 = 0; - let rc = unsafe { qjd_get_i64(d, bogus.as_ptr() as *const i8, bogus.len(), &mut v) }; - assert_eq!(rc, 2); // QJD_NOT_FOUND + let rc = unsafe { qjson_get_i64(d, bogus.as_ptr() as *const i8, bogus.len(), &mut v) }; + assert_eq!(rc, 2); // QJSON_NOT_FOUND - unsafe { qjd_free(d) }; + unsafe { qjson_free(d) }; } diff --git a/tests/json_test_suite.rs b/tests/json_test_suite.rs index c799395..0a07352 100644 --- a/tests/json_test_suite.rs +++ b/tests/json_test_suite.rs @@ -23,8 +23,8 @@ use std::fs; use std::path::Path; -use quickdecode::doc::Document; -use quickdecode::options::{Options, QJD_MODE_EAGER, QJD_MODE_LAZY}; +use qjson::doc::Document; +use qjson::options::{Options, QJSON_MODE_EAGER, QJSON_MODE_LAZY}; /// y_* files that we currently reject but shouldn't. /// Each is annotated with why and what follow-up would fix it. @@ -84,8 +84,8 @@ fn is_known_n_failure(path: &std::path::Path) -> bool { #[test] fn y_files_accepted_in_both_modes() { - let eager = Options { mode: QJD_MODE_EAGER, max_depth: 0 }; - let lazy = Options { mode: QJD_MODE_LAZY, max_depth: 0 }; + let eager = Options { mode: QJSON_MODE_EAGER, max_depth: 0 }; + let lazy = Options { mode: QJSON_MODE_LAZY, max_depth: 0 }; let mut failures = Vec::new(); let mut skipped = 0usize; @@ -119,7 +119,7 @@ fn y_files_accepted_in_both_modes() { #[test] fn n_files_rejected_in_eager_mode() { - let eager = Options { mode: QJD_MODE_EAGER, max_depth: 0 }; + let eager = Options { mode: QJSON_MODE_EAGER, max_depth: 0 }; let mut accepted = Vec::new(); let mut skipped = 0usize; @@ -149,7 +149,7 @@ fn n_files_rejected_in_eager_mode() { #[test] fn document_i_files_behavior() { // Implementation-defined cases — document what we do, do not assert. - let eager = Options { mode: QJD_MODE_EAGER, max_depth: 0 }; + let eager = Options { mode: QJSON_MODE_EAGER, max_depth: 0 }; for path in iter_files("i_") { let data = fs::read(&path).unwrap(); let verdict = match Document::parse_with_options(&data, &eager) { diff --git a/tests/lua/basic_spec.lua b/tests/lua/basic_spec.lua index f13f3da..a9f1b2d 100644 --- a/tests/lua/basic_spec.lua +++ b/tests/lua/basic_spec.lua @@ -1,51 +1,51 @@ -local qd = require("quickdecode") +local qjson = require("qjson") -describe("quickdecode basic", function() +describe("qjson basic", function() it("parses an object and gets a string field", function() - local d = qd.parse('{"a":"hello"}') + local d = qjson.parse('{"a":"hello"}') assert.are.equal("hello", d:get_str("a")) end) it("returns nil on missing path", function() - local d = qd.parse('{"a":1}') + local d = qjson.parse('{"a":1}') assert.is_nil(d:get_str("b")) end) it("errors on type mismatch", function() - local d = qd.parse('{"a":1}') + local d = qjson.parse('{"a":1}') assert.has_error(function() d:get_str("a") end) end) it("supports nested paths", function() - local d = qd.parse('{"body":{"model":"gpt"}}') + local d = qjson.parse('{"body":{"model":"gpt"}}') assert.are.equal("gpt", d:get_str("body.model")) end) it("supports array indexing", function() - local d = qd.parse('{"xs":[10,20,30]}') + local d = qjson.parse('{"xs":[10,20,30]}') assert.are.equal(20, d:get_i64("xs[1]")) end) it("cursor reuses shared prefix", function() - local d = qd.parse('{"body":{"a":1,"b":"two"}}') + local d = qjson.parse('{"body":{"a":1,"b":"two"}}') local b = d:open("body") assert.are.equal(1, b:get_i64("a")) assert.are.equal("two", b:get_str("b")) end) it("typeof reports correct types", function() - local d = qd.parse('{"s":"x","n":1,"f":1.5,"b":true,"z":null,"a":[],"o":{}}') - assert.are.equal(qd.T_STR, d:typeof("s")) - assert.are.equal(qd.T_NUM, d:typeof("n")) - assert.are.equal(qd.T_NUM, d:typeof("f")) - assert.are.equal(qd.T_BOOL, d:typeof("b")) - assert.are.equal(qd.T_NULL, d:typeof("z")) - assert.are.equal(qd.T_ARR, d:typeof("a")) - assert.are.equal(qd.T_OBJ, d:typeof("o")) + local d = qjson.parse('{"s":"x","n":1,"f":1.5,"b":true,"z":null,"a":[],"o":{}}') + assert.are.equal(qjson.T_STR, d:typeof("s")) + assert.are.equal(qjson.T_NUM, d:typeof("n")) + assert.are.equal(qjson.T_NUM, d:typeof("f")) + assert.are.equal(qjson.T_BOOL, d:typeof("b")) + assert.are.equal(qjson.T_NULL, d:typeof("z")) + assert.are.equal(qjson.T_ARR, d:typeof("a")) + assert.are.equal(qjson.T_OBJ, d:typeof("o")) end) it("len for objects and arrays", function() - local d = qd.parse('{"o":{"a":1,"b":2,"c":3},"a":[1,2,3,4]}') + local d = qjson.parse('{"o":{"a":1,"b":2,"c":3},"a":[1,2,3,4]}') assert.are.equal(3, d:len("o")) assert.are.equal(4, d:len("a")) end) diff --git a/tests/lua/cjson_compat_spec.lua b/tests/lua/cjson_compat_spec.lua index 0038de9..9498b35 100644 --- a/tests/lua/cjson_compat_spec.lua +++ b/tests/lua/cjson_compat_spec.lua @@ -1,29 +1,29 @@ -local qd = require("quickdecode") +local qjson = require("qjson") local cjson = require("cjson") -describe("quickdecode vs lua-cjson", function() +describe("qjson vs lua-cjson", function() it("agrees on simple string field", function() local s = '{"a":"x"}' - assert.are.equal(cjson.decode(s).a, qd.parse(s):get_str("a")) + assert.are.equal(cjson.decode(s).a, qjson.parse(s):get_str("a")) end) it("agrees on integer field", function() local s = '{"a":42}' - assert.are.equal(cjson.decode(s).a, qd.parse(s):get_i64("a")) + assert.are.equal(cjson.decode(s).a, qjson.parse(s):get_i64("a")) end) it("agrees on float field", function() local s = '{"a":1.5}' - assert.are.equal(cjson.decode(s).a, qd.parse(s):get_f64("a")) + assert.are.equal(cjson.decode(s).a, qjson.parse(s):get_f64("a")) end) it("agrees on bool field", function() local s = '{"a":true}' - assert.are.equal(cjson.decode(s).a, qd.parse(s):get_bool("a")) + assert.are.equal(cjson.decode(s).a, qjson.parse(s):get_bool("a")) end) it("agrees on nested path", function() local s = '{"body":{"model":"gpt"}}' - assert.are.equal(cjson.decode(s).body.model, qd.parse(s):get_str("body.model")) + assert.are.equal(cjson.decode(s).body.model, qjson.parse(s):get_str("body.model")) end) end) diff --git a/tests/lua/escape_spec.lua b/tests/lua/escape_spec.lua index a8c61b8..cd5b0f7 100644 --- a/tests/lua/escape_spec.lua +++ b/tests/lua/escape_spec.lua @@ -1,23 +1,23 @@ -local qd = require("quickdecode") +local qjson = require("qjson") -describe("quickdecode strings", function() +describe("qjson strings", function() it("decodes simple escape", function() - local d = qd.parse('{"a":"he\\nlo"}') + local d = qjson.parse('{"a":"he\\nlo"}') assert.are.equal("he\nlo", d:get_str("a")) end) it("decodes unicode escape", function() - local d = qd.parse('{"a":"\\u00e9"}') + local d = qjson.parse('{"a":"\\u00e9"}') assert.are.equal("\xc3\xa9", d:get_str("a")) end) it("decodes surrogate pair", function() - local d = qd.parse('{"a":"\\uD83D\\uDE00"}') + local d = qjson.parse('{"a":"\\uD83D\\uDE00"}') assert.are.equal("\xF0\x9F\x98\x80", d:get_str("a")) end) it("zero-copy for unescaped strings", function() - local d = qd.parse('{"a":"plain"}') + local d = qjson.parse('{"a":"plain"}') assert.are.equal("plain", d:get_str("a")) end) end) diff --git a/tests/lua/gc_spec.lua b/tests/lua/gc_spec.lua index ddc0938..365f20e 100644 --- a/tests/lua/gc_spec.lua +++ b/tests/lua/gc_spec.lua @@ -1,13 +1,13 @@ -local qd = require("quickdecode") +local qjson = require("qjson") -describe("quickdecode GC", function() - it("collects Doc without crashing and frees underlying qjd_doc", function() +describe("qjson GC", function() + it("collects Doc without crashing and frees underlying qjson_doc", function() -- Create and drop many Docs to exercise the ffi.gc finalizer path. -- A leak or double-free would surface as either crash, memory growth, -- or use-after-free under valgrind. Here we just confirm the loop -- completes and that values remain correct mid-loop. for i = 1, 200 do - local d = qd.parse(string.format('{"i":%d}', i)) + local d = qjson.parse(string.format('{"i":%d}', i)) assert.are.equal(i, d:get_i64("i")) d = nil -- drop reference end @@ -19,7 +19,7 @@ describe("quickdecode GC", function() -- Use a weak table to confirm the Doc is reachable for collection. local refs = setmetatable({}, { __mode = "v" }) do - local d = qd.parse('{"a":1}') + local d = qjson.parse('{"a":1}') refs[1] = d assert.are.equal(1, d:get_i64("a")) end diff --git a/tests/lua/lazy_table_spec.lua b/tests/lua/lazy_table_spec.lua index b0bf909..2769d39 100644 --- a/tests/lua/lazy_table_spec.lua +++ b/tests/lua/lazy_table_spec.lua @@ -1,39 +1,38 @@ -local qd = require("quickdecode") -local qt = qd -- keep tests reading naturally +local qjson = require("qjson") describe("LazyObject __index — scalars", function() it("reads a string field", function() - local t = qt.decode('{"k":"hello"}') + local t = qjson.decode('{"k":"hello"}') assert.are.equal("hello", t.k) end) it("reads a number field", function() - local t = qt.decode('{"n":42.5}') + local t = qjson.decode('{"n":42.5}') assert.are.equal(42.5, t.n) end) it("reads a boolean field", function() - local t = qt.decode('{"b":true,"c":false}') + local t = qjson.decode('{"b":true,"c":false}') assert.is_true(t.b) assert.is_false(t.c) end) it("returns nil for missing key", function() - local t = qt.decode('{"a":1}') + local t = qjson.decode('{"a":1}') assert.is_nil(t.missing) end) end) describe("LazyObject __index — nested containers", function() it("returns a LazyObject for a nested object", function() - local t = qt.decode('{"a":{"b":"x"}}') + local t = qjson.decode('{"a":{"b":"x"}}') local inner = t.a assert.is_table(inner) assert.are.equal("x", inner.b) end) it("returns a LazyArray for a nested array", function() - local t = qt.decode('{"xs":[10,20]}') + local t = qjson.decode('{"xs":[10,20]}') local xs = t.xs assert.is_table(xs) -- LazyArray __index is added in a later task; just verify it's @@ -43,27 +42,27 @@ end) describe("LazyArray __index", function() it("reads scalar elements by integer index (1-based)", function() - local t = qt.decode('[10,"x",true,null]') + local t = qjson.decode('[10,"x",true,null]') assert.are.equal(10, t[1]) assert.are.equal("x", t[2]) assert.is_true(t[3]) - assert.are.equal(qt.null, t[4]) + assert.are.equal(qjson.null, t[4]) end) it("returns nil for out-of-range index", function() - local t = qt.decode('[1,2,3]') + local t = qjson.decode('[1,2,3]') assert.is_nil(t[0]) assert.is_nil(t[4]) end) it("returns nil for non-integer key", function() - local t = qt.decode('[1,2,3]') + local t = qjson.decode('[1,2,3]') assert.is_nil(t.foo) assert.is_nil(t[1.5]) end) it("returns a nested LazyObject", function() - local t = qt.decode('[{"a":1},{"a":2}]') + local t = qjson.decode('[{"a":1},{"a":2}]') assert.are.equal(1, t[1].a) assert.are.equal(2, t[2].a) end) @@ -71,57 +70,57 @@ end) -- LuaJIT 5.1 only invokes __len on userdata; it ignores the metamethod on -- tables unless built with LUAJIT_ENABLE_LUA52COMPAT (OpenResty's default). --- Probe once so the `#t` cases only run where they can pass; qt.len(t) is +-- Probe once so the `#t` cases only run where they can pass; qjson.len(t) is -- the supported path everywhere. local LJ52_LEN = (#setmetatable({}, {__len = function() return 99 end}) == 99) -describe("qt.len", function() +describe("qjson.len", function() it("counts object keys", function() - local t = qt.decode('{"a":1,"b":2,"c":3}') - assert.are.equal(3, qt.len(t)) + local t = qjson.decode('{"a":1,"b":2,"c":3}') + assert.are.equal(3, qjson.len(t)) end) it("counts array elements", function() - local t = qt.decode('[10,20,30,40]') - assert.are.equal(4, qt.len(t)) + local t = qjson.decode('[10,20,30,40]') + assert.are.equal(4, qjson.len(t)) end) it("returns 0 for empty containers", function() - assert.are.equal(0, qt.len(qt.decode('{}'))) - assert.are.equal(0, qt.len(qt.decode('[]'))) + assert.are.equal(0, qjson.len(qjson.decode('{}'))) + assert.are.equal(0, qjson.len(qjson.decode('[]'))) end) it("falls back to # on a plain table", function() - assert.are.equal(3, qt.len({10, 20, 30})) + assert.are.equal(3, qjson.len({10, 20, 30})) end) end) describe("__len (LJ52 only)", function() it("counts object keys via #t", function() if not LJ52_LEN then return pending("LuaJIT built without LUAJIT_ENABLE_LUA52COMPAT") end - local t = qt.decode('{"a":1,"b":2,"c":3}') + local t = qjson.decode('{"a":1,"b":2,"c":3}') assert.are.equal(3, #t) end) it("counts array elements via #t", function() if not LJ52_LEN then return pending("LuaJIT built without LUAJIT_ENABLE_LUA52COMPAT") end - local t = qt.decode('[10,20,30,40]') + local t = qjson.decode('[10,20,30,40]') assert.are.equal(4, #t) end) it("returns 0 for empty containers via #t", function() if not LJ52_LEN then return pending("LuaJIT built without LUAJIT_ENABLE_LUA52COMPAT") end - assert.are.equal(0, #qt.decode('{}')) - assert.are.equal(0, #qt.decode('[]')) + assert.are.equal(0, #qjson.decode('{}')) + assert.are.equal(0, #qjson.decode('[]')) end) end) -describe("__pairs / qd.pairs over LazyObject", function() +describe("__pairs / qjson.pairs over LazyObject", function() it("iterates string keys in source order", function() - local t = qt.decode('{"a":1,"b":2,"c":3}') + local t = qjson.decode('{"a":1,"b":2,"c":3}') local keys = {} local values = {} - for k, v in qt.pairs(t) do + for k, v in qjson.pairs(t) do keys[#keys+1] = k values[#values+1] = v end @@ -130,8 +129,8 @@ describe("__pairs / qd.pairs over LazyObject", function() end) it("returns nested containers as lazy proxies, not materialized", function() - local t = qt.decode('{"a":{"x":1}}') - for _, v in qt.pairs(t) do + local t = qjson.decode('{"a":{"x":1}}') + for _, v in qjson.pairs(t) do assert.is_table(v) assert.are.equal(1, v.x) end @@ -139,23 +138,23 @@ describe("__pairs / qd.pairs over LazyObject", function() it("handles empty object", function() local count = 0 - for _ in qt.pairs(qt.decode('{}')) do count = count + 1 end + for _ in qjson.pairs(qjson.decode('{}')) do count = count + 1 end assert.are.equal(0, count) end) end) -describe("__ipairs / qd.ipairs over LazyArray", function() +describe("__ipairs / qjson.ipairs over LazyArray", function() it("iterates elements 1..n in order", function() - local t = qt.decode('[10,20,30]') + local t = qjson.decode('[10,20,30]') local got = {} - for i, v in qt.ipairs(t) do got[i] = v end + for i, v in qjson.ipairs(t) do got[i] = v end assert.are.same({10,20,30}, got) end) it("yields lazy proxies for nested containers", function() - local t = qt.decode('[{"a":1},{"a":2}]') + local t = qjson.decode('[{"a":1},{"a":2}]') local seen = {} - for _, v in qt.ipairs(t) do + for _, v in qjson.ipairs(t) do assert.is_table(v) seen[#seen+1] = v.a end @@ -164,14 +163,14 @@ describe("__ipairs / qd.ipairs over LazyArray", function() it("handles empty array", function() local count = 0 - for _ in qt.ipairs(qt.decode('[]')) do count = count + 1 end + for _ in qjson.ipairs(qjson.decode('[]')) do count = count + 1 end assert.are.equal(0, count) end) end) describe("__newindex — first-write materialization", function() it("converts LazyObject into a plain table preserving existing keys", function() - local t = qt.decode('{"a":1,"b":2}') + local t = qjson.decode('{"a":1,"b":2}') t.c = 3 assert.is_nil(getmetatable(t)) assert.are.equal(1, t.a) @@ -180,32 +179,32 @@ describe("__newindex — first-write materialization", function() end) it("nested containers remain lazy after parent materialization", function() - local t = qt.decode('{"inner":{"x":1}}') + local t = qjson.decode('{"inner":{"x":1}}') t.extra = "y" assert.is_nil(getmetatable(t)) local inner = t.inner - assert.are.equal(qt._LazyObject, getmetatable(inner)) + assert.are.equal(qjson._LazyObject, getmetatable(inner)) assert.are.equal(1, inner.x) end) it("LazyArray materializes preserving empty_array_mt", function() - local t = qt.decode('[]') + local t = qjson.decode('[]') t[1] = "x" - assert.are.equal(qt.empty_array_mt, getmetatable(t)) + assert.are.equal(qjson.empty_array_mt, getmetatable(t)) assert.are.equal("x", t[1]) end) it("simple write leaves other keys intact", function() - local t = qt.decode('{"a":1}') + local t = qjson.decode('{"a":1}') t.b = 2 assert.are.equal(1, t.a) assert.are.equal(2, t.b) end) end) -describe("qt.materialize", function() +describe("qjson.materialize", function() it("converts a LazyObject and its nested containers into real tables", function() - local m = qt.materialize(qt.decode('{"a":1,"b":{"c":[10,20]}}')) + local m = qjson.materialize(qjson.decode('{"a":1,"b":{"c":[10,20]}}')) assert.is_nil(getmetatable(m)) assert.are.equal(1, m.a) assert.is_nil(getmetatable(m.b)) @@ -214,91 +213,91 @@ describe("qt.materialize", function() end) it("tags empty arrays with empty_array_mt", function() - local m = qt.materialize(qt.decode('[]')) - assert.are.equal(qt.empty_array_mt, getmetatable(m)) + local m = qjson.materialize(qjson.decode('[]')) + assert.are.equal(qjson.empty_array_mt, getmetatable(m)) end) it("preserves cjson.null", function() - local m = qt.materialize(qt.decode('{"x":null}')) - assert.are.equal(qt.null, m.x) + local m = qjson.materialize(qjson.decode('{"x":null}')) + assert.are.equal(qjson.null, m.x) end) it("passes through scalars and plain tables unchanged", function() - assert.are.equal(42, qt.materialize(42)) - assert.are.equal("hi", qt.materialize("hi")) + assert.are.equal(42, qjson.materialize(42)) + assert.are.equal("hi", qjson.materialize("hi")) local raw = {1, 2, 3} - assert.are.equal(raw, qt.materialize(raw)) + assert.are.equal(raw, qjson.materialize(raw)) end) end) -describe("qd.encode — lazy proxy substring fast path", function() +describe("qjson.encode — lazy proxy substring fast path", function() it("re-emits the original JSON for an unmodified LazyObject", function() local src = '{"a":1,"b":[2,3],"c":"x"}' - local t = qt.decode(src) - assert.are.equal(src, qt.encode(t)) + local t = qjson.decode(src) + assert.are.equal(src, qjson.encode(t)) end) it("re-emits the original JSON for an unmodified LazyArray", function() local src = '[10,20,{"k":"v"}]' - local t = qt.decode(src) - assert.are.equal(src, qt.encode(t)) + local t = qjson.decode(src) + assert.are.equal(src, qjson.encode(t)) end) it("trims leading/trailing whitespace at the boundary", function() local src = ' {"a":1} ' - local t = qt.decode(src) + local t = qjson.decode(src) -- byte span is the value, not its outer whitespace. - assert.are.equal('{"a":1}', qt.encode(t)) + assert.are.equal('{"a":1}', qjson.encode(t)) end) end) -describe("qd.encode — scalars", function() +describe("qjson.encode — scalars", function() it("encodes strings with JSON escapes", function() - assert.are.equal('"hello"', qt.encode("hello")) - assert.are.equal('"a\\nb"', qt.encode("a\nb")) - assert.are.equal('"a\\"b"', qt.encode('a"b')) - assert.are.equal('"a\\\\b"', qt.encode("a\\b")) + assert.are.equal('"hello"', qjson.encode("hello")) + assert.are.equal('"a\\nb"', qjson.encode("a\nb")) + assert.are.equal('"a\\"b"', qjson.encode('a"b')) + assert.are.equal('"a\\\\b"', qjson.encode("a\\b")) end) it("encodes booleans", function() - assert.are.equal("true", qt.encode(true)) - assert.are.equal("false", qt.encode(false)) + assert.are.equal("true", qjson.encode(true)) + assert.are.equal("false", qjson.encode(false)) end) it("encodes numbers", function() - assert.are.equal("42", qt.encode(42)) - assert.are.equal("-3.14", qt.encode(-3.14)) + assert.are.equal("42", qjson.encode(42)) + assert.are.equal("-3.14", qjson.encode(-3.14)) end) - it("encodes qt.null as JSON null", function() - assert.are.equal("null", qt.encode(qt.null)) + it("encodes qjson.null as JSON null", function() + assert.are.equal("null", qjson.encode(qjson.null)) end) it("errors on unsupported values", function() - assert.has_error(function() qt.encode(function() end) end) + assert.has_error(function() qjson.encode(function() end) end) end) end) -describe("qd.encode — real and mixed tables", function() +describe("qjson.encode — real and mixed tables", function() it("encodes a real Lua object", function() local cjson = require("cjson") - local s = qt.encode({a = 1, b = "x"}) + local s = qjson.encode({a = 1, b = "x"}) assert.are.same({a = 1, b = "x"}, cjson.decode(s)) end) it("encodes a real Lua array", function() - assert.are.equal("[1,2,3]", qt.encode({1,2,3})) + assert.are.equal("[1,2,3]", qjson.encode({1,2,3})) end) it("encodes a hand-built empty array with empty_array_mt", function() - local arr = setmetatable({}, qt.empty_array_mt) - assert.are.equal("[]", qt.encode(arr)) + local arr = setmetatable({}, qjson.empty_array_mt) + assert.are.equal("[]", qjson.encode(arr)) end) it("encodes mixed lazy + materialized", function() - local t = qt.decode('{"keep":{"x":1},"changed":{"y":2}}') + local t = qjson.decode('{"keep":{"x":1},"changed":{"y":2}}') t.changed = "now a string" - local out = qt.encode(t) + local out = qjson.encode(t) local cjson = require("cjson") local parsed = cjson.decode(out) assert.are.same({x=1}, parsed.keep) @@ -308,7 +307,7 @@ end) local cjson = require("cjson") --- Deep-equal aware of cjson.null and empty_array_mt (which qd aliases). +-- Deep-equal aware of cjson.null and empty_array_mt (which qjson aliases). local function deep_equal(a, b) if a == b then return true end if type(a) ~= "table" or type(b) ~= "table" then return false end @@ -332,41 +331,41 @@ describe("cjson round-trip equivalence", function() } for _, src in ipairs(fixtures) do it("materialize matches cjson.decode for: " .. src:sub(1, 40), function() - local from_qd = qd.materialize(qd.decode(src)) + local from_qjson = qjson.materialize(qjson.decode(src)) local from_cj = cjson.decode(src) - assert.is_true(deep_equal(from_qd, from_cj)) + assert.is_true(deep_equal(from_qjson, from_cj)) end) it("encode round-trips for: " .. src:sub(1, 40), function() - local out = qd.encode(qd.decode(src)) - local back_qd = cjson.decode(out) + local out = qjson.encode(qjson.decode(src)) + local back_qjson = cjson.decode(out) local back_cj = cjson.decode(src) - assert.is_true(deep_equal(back_qd, back_cj)) + assert.is_true(deep_equal(back_qjson, back_cj)) end) end end) describe("sentinel handling", function() - it("JSON null reads as qd.null and encodes back", function() - local t = qd.decode('{"x":null}') - assert.are.equal(qd.null, t.x) - assert.are.equal('{"x":null}', qd.encode(t)) + it("JSON null reads as qjson.null and encodes back", function() + local t = qjson.decode('{"x":null}') + assert.are.equal(qjson.null, t.x) + assert.are.equal('{"x":null}', qjson.encode(t)) end) it("empty array stays an array through materialize and encode", function() - local t = qd.decode('{"xs":[]}') - local m = qd.materialize(t) - assert.are.equal(qd.empty_array_mt, getmetatable(m.xs)) - assert.are.equal('{"xs":[]}', qd.encode(t)) + local t = qjson.decode('{"xs":[]}') + local m = qjson.materialize(t) + assert.are.equal(qjson.empty_array_mt, getmetatable(m.xs)) + assert.are.equal('{"xs":[]}', qjson.encode(t)) end) end) -describe("qd.encode — nested mutations propagate", function() +describe("qjson.encode — nested mutations propagate", function() it("emits nested object mutation, not original bytes", function() local cjson = require("cjson") - local t = qd.decode('{"a":{"b":{"c":1}},"d":2}') + local t = qjson.decode('{"a":{"b":{"c":1}},"d":2}') t.a.b.c = 999 - local out = qd.encode(t) + local out = qjson.encode(t) local parsed = cjson.decode(out) assert.are.equal(999, parsed.a.b.c) assert.are.equal(2, parsed.d) @@ -374,9 +373,9 @@ describe("qd.encode — nested mutations propagate", function() it("emits nested array mutation", function() local cjson = require("cjson") - local t = qd.decode('{"xs":[10,20,30]}') + local t = qjson.decode('{"xs":[10,20,30]}') t.xs[2] = 999 - local out = qd.encode(t) + local out = qjson.encode(t) local parsed = cjson.decode(out) assert.are.equal(10, parsed.xs[1]) assert.are.equal(999, parsed.xs[2]) @@ -384,7 +383,7 @@ describe("qd.encode — nested mutations propagate", function() end) it("preserves cached proxy identity across parent materialization", function() - local t = qd.decode('{"a":{"x":1}}') + local t = qjson.decode('{"a":{"x":1}}') local inner = t.a t.c = 3 assert.are.equal(inner, t.a) diff --git a/tests/lua/options_spec.lua b/tests/lua/options_spec.lua index c689d2a..16ae663 100644 --- a/tests/lua/options_spec.lua +++ b/tests/lua/options_spec.lua @@ -1,36 +1,36 @@ -local qd = require "quickdecode" +local qjson = require "qjson" describe("parse with options", function() it("accepts no second arg (default eager)", function() - assert.is_not_nil(qd.parse('{"a":1}')) + assert.is_not_nil(qjson.parse('{"a":1}')) end) it("accepts an empty opts table", function() - assert.is_not_nil(qd.parse('{"a":1}', {})) + assert.is_not_nil(qjson.parse('{"a":1}', {})) end) it("accepts lazy=true and tolerates trailing content", function() -- Trailing content is eager-only; lazy must parse OK. - assert.is_not_nil(qd.parse('{}garbage', { lazy = true })) + assert.is_not_nil(qjson.parse('{}garbage', { lazy = true })) end) it("accepts max_depth", function() - assert.is_not_nil(qd.parse('[[[1]]]', { max_depth = 1024 })) + assert.is_not_nil(qjson.parse('[[[1]]]', { max_depth = 1024 })) end) it("rejects invalid mode key value", function() assert.has_error(function() - qd.parse('{}', { lazy = "yes please" }) + qjson.parse('{}', { lazy = "yes please" }) end) end) it("accepts lazy=true and max_depth combined", function() - assert.is_not_nil(qd.parse('[[1]]', { lazy = true, max_depth = 256 })) + assert.is_not_nil(qjson.parse('[[1]]', { lazy = true, max_depth = 256 })) end) it("rejects fractional max_depth", function() assert.has_error(function() - qd.parse('{}', { max_depth = 1.5 }) + qjson.parse('{}', { max_depth = 1.5 }) end) end) end) diff --git a/tests/rfc8259_compliance.rs b/tests/rfc8259_compliance.rs index 790511d..f83017e 100644 --- a/tests/rfc8259_compliance.rs +++ b/tests/rfc8259_compliance.rs @@ -11,11 +11,11 @@ //! RFC 8259 references are in section-paragraph form, e.g. RFC8259 §6 for //! the number grammar. -use quickdecode::doc::Document; -use quickdecode::options::{Options, QJD_MODE_EAGER, QJD_MODE_LAZY}; +use qjson::doc::Document; +use qjson::options::{Options, QJSON_MODE_EAGER, QJSON_MODE_LAZY}; -fn eager() -> Options { Options { mode: QJD_MODE_EAGER, max_depth: 0 } } -fn lazy() -> Options { Options { mode: QJD_MODE_LAZY, max_depth: 0 } } +fn eager() -> Options { Options { mode: QJSON_MODE_EAGER, max_depth: 0 } } +fn lazy() -> Options { Options { mode: QJSON_MODE_LAZY, max_depth: 0 } } /// Asserts the input is accepted in both modes. /// @@ -35,13 +35,13 @@ macro_rules! assert_accepts { /// Asserts the input is REJECTED by eager parse. /// -/// Usage: `assert_rejects_eager!("01", QJD_INVALID_NUMBER);` +/// Usage: `assert_rejects_eager!("01", QJSON_INVALID_NUMBER);` #[macro_export] macro_rules! assert_rejects_eager { ($input:expr, $expected_err:ident) => {{ - use quickdecode::error::qjd_err; + use qjson::error::qjson_err; let buf: &[u8] = $input.as_ref(); - let expected = qjd_err::$expected_err; + let expected = qjson_err::$expected_err; match Document::parse_with_options(buf, &eager()) { Err(e) if e == expected => {} Err(other) => panic!( @@ -80,38 +80,38 @@ fn smoke_rejects_unmatched_brace_both_modes() { } #[test] -#[should_panic(expected = "expected QJD_INVALID_NUMBER")] +#[should_panic(expected = "expected QJSON_INVALID_NUMBER")] fn macro_rejects_wrong_error_code() { // Sanity: passing the wrong expected variant must panic. - // `{` is rejected as QJD_PARSE_ERROR, NOT QJD_INVALID_NUMBER. + // `{` is rejected as QJSON_PARSE_ERROR, NOT QJSON_INVALID_NUMBER. // With the buggy macro, this test would NOT panic (false positive // — the macro would silently bind whatever Err came back). - assert_rejects_eager!("{", QJD_INVALID_NUMBER); + assert_rejects_eager!("{", QJSON_INVALID_NUMBER); } // ── Phase 3: nesting depth ─────────────────────────────────── #[test] fn rejects_deeply_nested_at_default_limit() { - use quickdecode::error::qjd_err; + use qjson::error::qjson_err; let mut buf = String::new(); for _ in 0..1100 { buf.push('['); } for _ in 0..1100 { buf.push(']'); } match Document::parse_with_options(buf.as_bytes(), &eager()) { - Err(qjd_err::QJD_NESTING_TOO_DEEP) => {} - other => panic!("expected QJD_NESTING_TOO_DEEP, got {:?}", other.err()), + Err(qjson_err::QJSON_NESTING_TOO_DEEP) => {} + other => panic!("expected QJSON_NESTING_TOO_DEEP, got {:?}", other.err()), } } #[test] fn lazy_mode_also_enforces_max_depth() { - use quickdecode::error::qjd_err; + use qjson::error::qjson_err; let mut buf = String::new(); for _ in 0..1100 { buf.push('['); } for _ in 0..1100 { buf.push(']'); } assert_eq!( Document::parse_with_options(buf.as_bytes(), &lazy()).err().unwrap(), - qjd_err::QJD_NESTING_TOO_DEEP, + qjson_err::QJSON_NESTING_TOO_DEEP, ); } @@ -120,7 +120,7 @@ fn accepts_nested_at_configured_limit() { let mut buf = String::new(); for _ in 0..256 { buf.push('['); } for _ in 0..256 { buf.push(']'); } - let opts = Options { mode: QJD_MODE_EAGER, max_depth: 256 }; + let opts = Options { mode: QJSON_MODE_EAGER, max_depth: 256 }; assert!(Document::parse_with_options(buf.as_bytes(), &opts).is_ok()); } @@ -129,7 +129,7 @@ fn rejects_when_one_past_configured_limit() { let mut buf = String::new(); for _ in 0..33 { buf.push('['); } for _ in 0..33 { buf.push(']'); } - let opts = Options { mode: QJD_MODE_EAGER, max_depth: 32 }; + let opts = Options { mode: QJSON_MODE_EAGER, max_depth: 32 }; assert!(Document::parse_with_options(buf.as_bytes(), &opts).is_err()); } @@ -137,23 +137,23 @@ fn rejects_when_one_past_configured_limit() { #[test] fn eager_rejects_trailing_content() { - use quickdecode::error::qjd_err; + use qjson::error::qjson_err; assert_eq!( Document::parse_with_options(b"{}garbage", &eager()).err().unwrap(), - qjd_err::QJD_TRAILING_CONTENT, + qjson_err::QJSON_TRAILING_CONTENT, ); } #[test] fn eager_rejects_multiple_root_values() { - use quickdecode::error::qjd_err; + use qjson::error::qjson_err; assert_eq!( Document::parse_with_options(b"1 2", &eager()).err().unwrap(), - qjd_err::QJD_TRAILING_CONTENT, + qjson_err::QJSON_TRAILING_CONTENT, ); assert_eq!( Document::parse_with_options(b"true false", &eager()).err().unwrap(), - qjd_err::QJD_TRAILING_CONTENT, + qjson_err::QJSON_TRAILING_CONTENT, ); } @@ -187,14 +187,14 @@ fn eager_accepts_canonical_numbers() { #[test] fn eager_rejects_invalid_numbers() { - use quickdecode::error::qjd_err; + use qjson::error::qjson_err; for s in ["+1", "01", "00", ".5", "1.", "1.e5", "0x1F", "NaN", "Infinity", "-Infinity", "1e", "1e+"] { let input = format!("[{}]", s); match Document::parse_with_options(input.as_bytes(), &eager()) { - Err(qjd_err::QJD_INVALID_NUMBER) => {} + Err(qjson_err::QJSON_INVALID_NUMBER) => {} Err(other) => panic!( - "expected QJD_INVALID_NUMBER for {:?}, got {:?}", input, other), + "expected QJSON_INVALID_NUMBER for {:?}, got {:?}", input, other), Ok(_) => panic!("EAGER unexpectedly accepted {:?}", input), } } @@ -213,33 +213,33 @@ fn lazy_defers_invalid_number_until_access() { #[test] fn eager_rejects_raw_tab_in_string() { - use quickdecode::error::qjd_err; + use qjson::error::qjson_err; let input = b"[\"a\tb\"]"; match Document::parse_with_options(input, &eager()) { - Err(qjd_err::QJD_INVALID_STRING) => {} - Err(other) => panic!("expected QJD_INVALID_STRING, got {:?}", other), + Err(qjson_err::QJSON_INVALID_STRING) => {} + Err(other) => panic!("expected QJSON_INVALID_STRING, got {:?}", other), Ok(_) => panic!("EAGER unexpectedly accepted raw tab in string"), } } #[test] fn eager_rejects_raw_null_in_string() { - use quickdecode::error::qjd_err; + use qjson::error::qjson_err; let input = b"[\"a\x00b\"]"; match Document::parse_with_options(input, &eager()) { - Err(qjd_err::QJD_INVALID_STRING) => {} - Err(other) => panic!("expected QJD_INVALID_STRING, got {:?}", other), + Err(qjson_err::QJSON_INVALID_STRING) => {} + Err(other) => panic!("expected QJSON_INVALID_STRING, got {:?}", other), Ok(_) => panic!("EAGER unexpectedly accepted raw null in string"), } } #[test] fn eager_rejects_invalid_utf8_in_string() { - use quickdecode::error::qjd_err; + use qjson::error::qjson_err; let input = &[b'[', b'"', 0xC0, 0xC0, b'"', b']']; match Document::parse_with_options(input, &eager()) { - Err(qjd_err::QJD_INVALID_UTF8) => {} - Err(other) => panic!("expected QJD_INVALID_UTF8, got {:?}", other), + Err(qjson_err::QJSON_INVALID_UTF8) => {} + Err(other) => panic!("expected QJSON_INVALID_UTF8, got {:?}", other), Ok(_) => panic!("EAGER unexpectedly accepted invalid UTF-8 in string"), } } @@ -264,61 +264,61 @@ fn lazy_accepts_raw_tab_but_decode_fails() { #[test] fn eager_rejects_uppercase_true_as_parse_error() { - use quickdecode::error::qjd_err; + use qjson::error::qjson_err; let r = Document::parse_with_options(b"TRUE", &eager()); match r { - Err(qjd_err::QJD_PARSE_ERROR) => {} - other => panic!("expected QJD_PARSE_ERROR, got {:?}", other.err()), + Err(qjson_err::QJSON_PARSE_ERROR) => {} + other => panic!("expected QJSON_PARSE_ERROR, got {:?}", other.err()), } } #[test] fn eager_rejects_uppercase_false_as_parse_error() { - use quickdecode::error::qjd_err; + use qjson::error::qjson_err; let r = Document::parse_with_options(b"False", &eager()); match r { - Err(qjd_err::QJD_PARSE_ERROR) => {} - other => panic!("expected QJD_PARSE_ERROR, got {:?}", other.err()), + Err(qjson_err::QJSON_PARSE_ERROR) => {} + other => panic!("expected QJSON_PARSE_ERROR, got {:?}", other.err()), } } #[test] fn eager_rejects_uppercase_null_as_parse_error() { - use quickdecode::error::qjd_err; + use qjson::error::qjson_err; let r = Document::parse_with_options(b"NULL", &eager()); match r { - Err(qjd_err::QJD_PARSE_ERROR) => {} - other => panic!("expected QJD_PARSE_ERROR, got {:?}", other.err()), + Err(qjson_err::QJSON_PARSE_ERROR) => {} + other => panic!("expected QJSON_PARSE_ERROR, got {:?}", other.err()), } } #[test] fn eager_rejects_undefined_as_parse_error() { - use quickdecode::error::qjd_err; + use qjson::error::qjson_err; let r = Document::parse_with_options(b"undefined", &eager()); match r { - Err(qjd_err::QJD_PARSE_ERROR) => {} - other => panic!("expected QJD_PARSE_ERROR, got {:?}", other.err()), + Err(qjson_err::QJSON_PARSE_ERROR) => {} + other => panic!("expected QJSON_PARSE_ERROR, got {:?}", other.err()), } } #[test] fn eager_rejects_nan_as_invalid_number() { - use quickdecode::error::qjd_err; + use qjson::error::qjson_err; let r = Document::parse_with_options(b"NaN", &eager()); match r { - Err(qjd_err::QJD_INVALID_NUMBER) => {} - other => panic!("expected QJD_INVALID_NUMBER, got {:?}", other.err()), + Err(qjson_err::QJSON_INVALID_NUMBER) => {} + other => panic!("expected QJSON_INVALID_NUMBER, got {:?}", other.err()), } } #[test] fn eager_rejects_infinity_as_invalid_number() { - use quickdecode::error::qjd_err; + use qjson::error::qjson_err; let r = Document::parse_with_options(b"Infinity", &eager()); match r { - Err(qjd_err::QJD_INVALID_NUMBER) => {} - other => panic!("expected QJD_INVALID_NUMBER, got {:?}", other.err()), + Err(qjson_err::QJSON_INVALID_NUMBER) => {} + other => panic!("expected QJSON_INVALID_NUMBER, got {:?}", other.err()), } } @@ -394,7 +394,7 @@ mod structural { // Eager catches the empty gap after ':'; lazy defers (structural-only rule). #[test] fn missing_value() { - assert_rejects_eager!("{\"a\":}", QJD_PARSE_ERROR); + assert_rejects_eager!("{\"a\":}", QJSON_PARSE_ERROR); } // RFC 8259 §4: colon between key and value is mandatory. @@ -403,7 +403,7 @@ mod structural { // it can only close ObjAfterOpen/ObjAfterValue. #[test] fn missing_colon() { - assert_rejects_eager!("{\"a\"}", QJD_PARSE_ERROR); + assert_rejects_eager!("{\"a\"}", QJSON_PARSE_ERROR); } // RFC 8259 §5: a leading comma in an array is invalid. @@ -411,7 +411,7 @@ mod structural { // heuristic in check_gap. #[test] fn leading_comma_array_empty() { - assert_rejects_eager!("[,]", QJD_PARSE_ERROR); + assert_rejects_eager!("[,]", QJSON_PARSE_ERROR); } // [,1] — leading comma followed by a value: the grammar-aware @@ -419,28 +419,28 @@ mod structural { // state (only a value or `]` is allowed after `[`). #[test] fn leading_comma_array_with_value() { - assert_rejects_eager!("[,1]", QJD_PARSE_ERROR); + assert_rejects_eager!("[,1]", QJSON_PARSE_ERROR); } // RFC 8259 §5: trailing comma in an array is invalid. #[test] fn trailing_comma_array() { - assert_rejects_eager!("[1,]", QJD_PARSE_ERROR); + assert_rejects_eager!("[1,]", QJSON_PARSE_ERROR); } // RFC 8259 §4: trailing comma in an object is invalid. #[test] fn trailing_comma_object() { - assert_rejects_eager!("{\"a\":1,}", QJD_PARSE_ERROR); + assert_rejects_eager!("{\"a\":1,}", QJSON_PARSE_ERROR); } // RFC 8259 §5: array elements must be separated by exactly one comma. // [1 2] contains a space-separated pair that validate_number rejects as - // QJD_INVALID_NUMBER (not QJD_PARSE_ERROR) — the element IS rejected by + // QJSON_INVALID_NUMBER (not QJSON_PARSE_ERROR) — the element IS rejected by // eager, just with a different error code. #[test] fn missing_comma_in_array_rejected() { - // We assert only that eager rejects; the exact code is QJD_INVALID_NUMBER + // We assert only that eager rejects; the exact code is QJSON_INVALID_NUMBER // because the "1 2" token fails number validation (space within number). let input = b"[1 2]"; assert!( @@ -455,7 +455,7 @@ mod structural { // a key/value-position quote is not legal there. #[test] fn missing_comma_in_object() { - assert_rejects_eager!("{\"a\":1\"b\":2}", QJD_PARSE_ERROR); + assert_rejects_eager!("{\"a\":1\"b\":2}", QJSON_PARSE_ERROR); } } @@ -510,33 +510,33 @@ mod literals { #[test] fn true_must_be_lowercase() { - assert_rejects_eager!("TRUE", QJD_PARSE_ERROR); - assert_rejects_eager!("True", QJD_PARSE_ERROR); - assert_rejects_eager!("tRuE", QJD_PARSE_ERROR); + assert_rejects_eager!("TRUE", QJSON_PARSE_ERROR); + assert_rejects_eager!("True", QJSON_PARSE_ERROR); + assert_rejects_eager!("tRuE", QJSON_PARSE_ERROR); } #[test] fn false_must_be_lowercase() { - assert_rejects_eager!("FALSE", QJD_PARSE_ERROR); - assert_rejects_eager!("False", QJD_PARSE_ERROR); + assert_rejects_eager!("FALSE", QJSON_PARSE_ERROR); + assert_rejects_eager!("False", QJSON_PARSE_ERROR); } #[test] fn null_must_be_lowercase() { - assert_rejects_eager!("NULL", QJD_PARSE_ERROR); - assert_rejects_eager!("Null", QJD_PARSE_ERROR); + assert_rejects_eager!("NULL", QJSON_PARSE_ERROR); + assert_rejects_eager!("Null", QJSON_PARSE_ERROR); } // JavaScript-ism: "nil" is not a valid JSON value. #[test] fn nil_rejected() { - assert_rejects_eager!("nil", QJD_PARSE_ERROR); + assert_rejects_eager!("nil", QJSON_PARSE_ERROR); } // JavaScript-ism: "undefined" is not a valid JSON value. #[test] fn undefined_rejected() { - assert_rejects_eager!("undefined", QJD_PARSE_ERROR); + assert_rejects_eager!("undefined", QJSON_PARSE_ERROR); } } @@ -590,23 +590,23 @@ mod strings { // JSON does not allow single-quoted strings (JavaScript-ism). #[test] fn single_quoted_string_rejected() { - assert_rejects_eager!("'hello'", QJD_PARSE_ERROR); + assert_rejects_eager!("'hello'", QJSON_PARSE_ERROR); } // RFC 8259 §7: control characters (U+0000–U+001F) must be escaped. // A raw tab (0x09) inside a string is forbidden. #[test] fn raw_control_char_rejected() { - use quickdecode::error::qjd_err; + use qjson::error::qjson_err; let with_tab = b"[\"a\tb\"]"; let with_null = b"[\"a\x00b\"]"; match Document::parse_with_options(with_tab, &eager()) { - Err(qjd_err::QJD_INVALID_STRING) => {} - other => panic!("expected QJD_INVALID_STRING for raw tab, got {:?}", other.err()), + Err(qjson_err::QJSON_INVALID_STRING) => {} + other => panic!("expected QJSON_INVALID_STRING for raw tab, got {:?}", other.err()), } match Document::parse_with_options(with_null, &eager()) { - Err(qjd_err::QJD_INVALID_STRING) => {} - other => panic!("expected QJD_INVALID_STRING for raw NUL, got {:?}", other.err()), + Err(qjson_err::QJSON_INVALID_STRING) => {} + other => panic!("expected QJSON_INVALID_STRING for raw NUL, got {:?}", other.err()), } } @@ -659,56 +659,56 @@ mod numbers { // §6: leading '+' is not allowed. #[test] fn leading_plus_rejected() { - assert_rejects_eager!("[+1]", QJD_INVALID_NUMBER); + assert_rejects_eager!("[+1]", QJSON_INVALID_NUMBER); } // §6: leading zeros are not allowed (except bare "0"). #[test] fn leading_zero_rejected() { - assert_rejects_eager!("[01]", QJD_INVALID_NUMBER); - assert_rejects_eager!("[00]", QJD_INVALID_NUMBER); - assert_rejects_eager!("[007]", QJD_INVALID_NUMBER); + assert_rejects_eager!("[01]", QJSON_INVALID_NUMBER); + assert_rejects_eager!("[00]", QJSON_INVALID_NUMBER); + assert_rejects_eager!("[007]", QJSON_INVALID_NUMBER); } // §6: fraction requires at least one digit after the dot. #[test] fn trailing_dot_rejected() { - assert_rejects_eager!("[1.]", QJD_INVALID_NUMBER); - assert_rejects_eager!("[1.e5]", QJD_INVALID_NUMBER); + assert_rejects_eager!("[1.]", QJSON_INVALID_NUMBER); + assert_rejects_eager!("[1.e5]", QJSON_INVALID_NUMBER); } // §6: fraction cannot start without an integer part. #[test] fn leading_dot_rejected() { - assert_rejects_eager!("[.5]", QJD_INVALID_NUMBER); + assert_rejects_eager!("[.5]", QJSON_INVALID_NUMBER); } // §6: exponent requires at least one digit. #[test] fn incomplete_exponent_rejected() { - assert_rejects_eager!("[1e]", QJD_INVALID_NUMBER); - assert_rejects_eager!("[1e+]", QJD_INVALID_NUMBER); - assert_rejects_eager!("[1e-]", QJD_INVALID_NUMBER); + assert_rejects_eager!("[1e]", QJSON_INVALID_NUMBER); + assert_rejects_eager!("[1e+]", QJSON_INVALID_NUMBER); + assert_rejects_eager!("[1e-]", QJSON_INVALID_NUMBER); } // Hex notation is not part of the JSON number grammar. #[test] fn hex_notation_rejected() { - assert_rejects_eager!("[0x1F]", QJD_INVALID_NUMBER); - assert_rejects_eager!("[0xFF]", QJD_INVALID_NUMBER); + assert_rejects_eager!("[0x1F]", QJSON_INVALID_NUMBER); + assert_rejects_eager!("[0xFF]", QJSON_INVALID_NUMBER); } // Non-finite values are not part of JSON. #[test] fn non_finite_rejected() { - assert_rejects_eager!("[NaN]", QJD_INVALID_NUMBER); - assert_rejects_eager!("[Infinity]", QJD_INVALID_NUMBER); - assert_rejects_eager!("[-Infinity]", QJD_INVALID_NUMBER); + assert_rejects_eager!("[NaN]", QJSON_INVALID_NUMBER); + assert_rejects_eager!("[Infinity]", QJSON_INVALID_NUMBER); + assert_rejects_eager!("[-Infinity]", QJSON_INVALID_NUMBER); } // Lone minus is not a valid number. #[test] fn lone_minus_rejected() { - assert_rejects_eager!("[-]", QJD_INVALID_NUMBER); + assert_rejects_eager!("[-]", QJSON_INVALID_NUMBER); } } diff --git a/tests/scanner_crosscheck.rs b/tests/scanner_crosscheck.rs index f27b737..7c9ab85 100644 --- a/tests/scanner_crosscheck.rs +++ b/tests/scanner_crosscheck.rs @@ -2,7 +2,7 @@ use proptest::prelude::*; #[cfg(all(target_arch = "x86_64", feature = "avx2"))] -use quickdecode::__test_api::{Scanner, ScalarScanner, Avx2Scanner}; +use qjson::__test_api::{Scanner, ScalarScanner, Avx2Scanner}; #[cfg(all(target_arch = "x86_64", feature = "avx2"))] proptest! { @@ -63,7 +63,7 @@ fn valid_jsonish() -> impl Strategy { use proptest::prelude::*; #[cfg(target_arch = "aarch64")] -use quickdecode::__test_api::{Scanner, ScalarScanner, NeonScanner}; +use qjson::__test_api::{Scanner, ScalarScanner, NeonScanner}; #[cfg(target_arch = "aarch64")] proptest! {