JSON
rusty-json-manifest is Cruft's JSON parser, the code that reads the package.json, cruft-lock.json, and internal records Cruft consumes. It parses strict RFC 8259, preserves object key order and duplicate keys, and caps nesting depth at 256 and input at 8 MiB so a hostile manifest cannot crash or exhaust the parser.
Every npm install reads manifests you never authored: the package.json of every transitive dependency, a lockfile, a registry response. Somewhere below your code, a JSON parser turns those bytes into objects. On Node that parser is V8's JSON.parse, hardened by years of adversarial exposure. In Cruft it is a crate, rusty-json-manifest. This page is about what that crate does, what it does not do, and where you should still be nervous.
Alpha, do not rely in production.rusty-json-manifest(Cruft 0.0.10) replaces theserde_jsondependency for the JSON that Cruft itself consumes:package.json,cruft-lock.json, and Cruft's own internal records. It is not externally audited and has near-zero interop testing. Treat its properties as unproven except where this page points at an in-tree test or an enforcing code path. Do not point it at untrusted input in production.
What this crate takes from serde_json, and what it leaves out
serde_json gives you two things: a Value enum you can walk, and the derive-macro data model (#[derive(Serialize, Deserialize)]) that maps arbitrary Rust structs to and from JSON. rusty-json-manifest gives you the first and deliberately withholds the second.
The crate's own header says so: it covers the consumed serde_json::Value-like manifest surface and intentionally does not implement Serde's framework or data-model surface. So there is no #[derive], no Deserializer, no visitor pattern. There is a JsonValue enum with six variants:
Around it sit a from_str / from_slice parser, field accessors (get, as_str, as_array, as_u64...), and two writers (to_compact_string, to_pretty_string). For construction there is a json! builder macro plus an IntoJsonValue trait and to_value for turning scalars and maps into a JsonValue. What is deliberately absent is Serde's derive and data-model surface: it reads and builds manifests, and does not serialize arbitrary structs.
Object order and duplicate keys are preserved
A hash-map mental model gets duplicate-key handling wrong. JSON objects can carry the same key twice, and a manifest is exactly where that matters (a hand-edited or hostile package.json). serde_json with a Map keeps the last value and drops key order unless you opt into a feature.
This crate's Map is a vector of key/value pairs, not a hash map. Insertion order is preserved, and on a parse it keeps every duplicate: get returns the first binding for a key, get_last returns the last. Parsing {"b":1,"a":2,"b":3} returns keys ["b","a","b"] with get("b") == 1 and get_last("b") == 3. The cost: lookups and insert are O(n) linear scans (Map::insert), so building a large object through repeated insert (or the json! macro) is O(n^2). The parse path itself pushes directly and avoids that, so parsing a big manifest stays linear.
Strict RFC 8259 parsing
This parser is stricter than the real thing. This is strict RFC 8259, and it rejects things a forgiving reader might accept.
- Leading-zero numbers (
01), bare-dot fractions (1.), and empty exponents (1e) are errors. - A lone high or low surrogate in a
\uescape is rejected; a valid surrogate pair is combined into the astral scalar; raw control characters inside a string are errors. - Trailing content after the value, trailing commas, and a missing
:are all errors.
Numbers are kept as their raw string slice and only parsed to f64 / u64 on demand via as_f64 / as_u64, so large integers are not silently mangled at parse time. What it does NOT do: comments, trailing commas, single quotes, NaN/Infinity, or any JSON5 extension. If Cruft ever needs to read a tsconfig.json with comments, this parser is not the tool.
The DoS surface: recursion and input size
The manifest parser is an attack surface: a hostile dependency ships a package.json, and the installer parses it before anyone has read a line of it. Two classic failures live here: unbounded recursion (deeply nested [[[[...]]]] overflows the native stack) and unbounded input size. In Rust a stack overflow is an uncatchable abort, so it cannot be turned into a caught error: it takes the process down.
This crate closes both, and the guards are enforced in the source you are reading.
- Depth cap. The
Parsercarries adepthcounter and aMAX_DEPTHof 256. Every array and object entry callsenter_nested, which returns aJsonError("json nesting depth exceeded") once the bound is hit rather than recursing another frame. Feeding 1100 nested arrays and 1100 nested objects yields a deterministic error, not a crash. - Input-size cap.
DEFAULT_MAX_JSON_BYTESis 8 MiB. Bothparse_value_strandfrom_slicecallvalidate_input_sizebefore any UTF-8 conversion or value allocation, returning a "parser limit" error above the cap, so an oversized input is rejected up front.
Limitations
The maturity baseline is serde_json: years of production exposure, an enormous fuzz corpus, wide interop. This crate has a small set of unit tests and near-zero interop or conformance-vector testing. That gap is the main thing to hold in mind.
Specifics:
- No fuzz target and no spec-vector suite for this crate. The tests are a handful of chosen examples. The depth and size guards rest on those cases, not on a fuzzer sweeping the input space. Correctness against the full RFC 8259 corner space (deep number precision, every escape form, malformed UTF-8 placement) is unproven beyond the cited cases.
- One
unwrap()on the parse path.parse_numberends withstr::from_utf8(&self.bytes[start..self.pos]).unwrap(). It is reached only after the scanner has advanced over ASCII digit/sign/./ebytes, so the slice is ASCII by construction and the unwrap is not proven reachable on malformed input. It is not backed by a fuzz proof, so read it as "no panic found, not no panic possible." - No streaming, no
serdedata model. Whole input in memory, whole value in memory, up to the 8 MiB cap. Anything needing incremental parse or struct derivation must look elsewhere. - Scope is Cruft's own manifests. It is exercised against
package.json/cruft-lock.jsonand Cruft's own records, not against the long tail of JSON the ecosystem produces. It is sound for that surface and unproven outside it.