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 the serde_json dependency 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:

JsonValue
NullBoolNumberStringArrayObject

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 \u escape 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 Parser carries a depth counter and a MAX_DEPTH of 256. Every array and object entry calls enter_nested, which returns a JsonError ("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_BYTES is 8 MiB. Both parse_value_str and from_slice call validate_input_size before 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_number ends with str::from_utf8(&self.bytes[start..self.pos]).unwrap(). It is reached only after the scanner has advanced over ASCII digit/sign/./e bytes, 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 serde data 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.json and Cruft's own records, not against the long tail of JSON the ecosystem produces. It is sound for that surface and unproven outside it.