1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
//! Admission control for JSON that is about to be signed or verified.
//!
//! One document has one canonical form, and a signature means nothing unless
//! every party agrees which bytes those are. RFC 8785 says how to write them.
//! This crate decides which documents are allowed to have them, refuses the rest
//! on the raw bytes, and then hands the writing to
//! [`serde_json_canonicalizer`].
//!
//! # Why a separate layer
//!
//! The two canonicalizers on crates.io are serde `Serializer`s. That is the
//! right shape for their job and it fixes where they can stand: a value has
//! already been parsed before either of them is reached, and the faults that
//! matter to a verifier are the ones that vanish during that parse. A repeated
//! object member is gone -- the map kept one of the two and nothing downstream
//! can tell it happened. There is no layer below them to put the check in, so the
//! check goes above them, on the bytes, which is this crate.
//!
//! Measured against `serde_json_canonicalizer` 0.3.2 on 2026-09-19 (the run is
//! pinned in `tests/incumbent_differential.rs`, which fails if any of it stops
//! being true):
//!
//! | input | the delegate | this crate |
//! |---|---|---|
//! | `{"a":1,"a":2}` | `{"a":2}` via `pipe`, `{"a":1}` straight into its serializer | [`Error::DuplicateMember`] |
//! | 129 nested arrays | accepted by `to_vec`; 10,000 aborts the process | [`Error::TooDeep`] |
//! | `9007199254740993` | `9007199254740992`, a different integer | [`Error::UnsafeInteger`] under [`admit_ijson`]; [`admit`] rewrites it the same way |
//! | 12 MB of array | accepted | [`Error::TooLarge`] |
//!
//! The third row's right-hand cell names [`admit_ijson`] and the qualification is
//! load-bearing: the RFC 8785 default *admits* that token and canonicalizes it to
//! `9007199254740992`, the same different integer the delegate produces, because
//! section 3.2.2.3 defers to ECMAScript and ECMAScript has one numeric type. The
//! The first row is the one to read twice. One document, one crate, two canonical
//! forms depending on which of its entry points you used, and both of them
//! signable.
//!
//! What the delegate gets right, it gets right for us: it formats numbers through
//! `ryu_js`, which is the ECMAScript `Number::toString` variant RFC 8785 section
//! 3.2.2.3 requires, and it reads the RFC's own awkward reference number
//! `333333333.33333329` correctly. This crate stands on that rather than carrying
//! a third copy of it.
//!
//! # What it refuses
//!
//! 1. **A repeated object member** ([`Error::DuplicateMember`]). Collapsing a
//! repeat to last-wins is a split view: two parties read the same bytes as two
//! different documents and a signature over either reading verifies.
//! Comparison is on the *decoded* name, so `"a"` and `"a"` are one member.
//! 2. **Nesting past a cap** ([`DEFAULT_MAX_DEPTH`], 128 -- the value RFC 8785's
//! in-toto profile made normative). Without a cap a crafted document
//! overflows the stack, which no caller can catch and which happens before any
//! signature is checked.
//! 3. **A string whose bytes are not Unicode scalar values**
//! ([`Error::StringNotScalar`]): an unpaired surrogate escape, a surrogate
//! written directly in UTF-8, an overlong form, a raw control character.
//! Checked before any decode, because a decoder that substitutes U+FFFD
//! leaves `"\ud800"`, `"\udc00"` and a literal U+FFFD canonicalizing to
//! identical bytes.
//! 4. **A number with no finite double** ([`Error::NonFiniteNumber`]), and under
//! the opt-in RFC 7493 profile ([`admit_ijson`]) an integer at or above 2^53
//! ([`Error::UnsafeInteger`]) or a Unicode noncharacter.
//! 5. **An input past a size cap** ([`Error::TooLarge`]). A depth cap bounds the
//! stack; a size cap bounds the heap.
//!
//! # Examples
//!
//! ```
//! use jcs_admit::{admit, Error};
//!
//! assert_eq!(admit(br#"{ "b": 1, "a": [1.0, 1e30] }"#)?, br#"{"a":[1,1e+30],"b":1}"#);
//!
//! // The refusal the layer exists for, and it names the member.
//! assert!(matches!(
//! admit(br#"{"a":1,"a":2}"#),
//! Err(Error::DuplicateMember { .. })
//! ));
//! # Ok::<(), Error>(())
//! ```
pub use Error;
/// The default depth cap: at most 128 nested arrays and objects.
///
/// Not arbitrary. 128 is the nesting limit `serde_json` enforces by default, it
/// is the `MAX_DEPTH` the in-toto attestation specification carries as normative
/// text, it is far below the stack-overflow point so the counter trips with an
/// ordinary error first, and it is orders of magnitude above any real signed
/// payload. Three values exist in the wild -- 128, 256, and none at all -- and
/// the third is a crash.
pub const DEFAULT_MAX_DEPTH: usize = 128;
/// The deepest nesting this crate honours, whatever a caller configures.
///
/// A depth cap is only a refusal while it is below the stack. The admission walk
/// recurses, one frame per open container, so a cap above the stack is not a cap:
/// measured on this machine before the ceiling existed, an unbounded cap on
/// 19,414 nested arrays aborted the process with "fatal runtime error: stack
/// overflow" -- not an `Err`, not catchable, reached through safe API in a crate
/// that forbids `unsafe` entirely. [`Options::max_depth`] saturates here: four
/// times the default, sized against the *unoptimized* build because that is the
/// profile a caller's own tests run under.
pub const MAX_SUPPORTED_DEPTH: usize = 512;
/// The default size cap on untrusted input, 20 MiB.
pub const DEFAULT_MAX_BYTES: usize = 20 << 20;
/// What to refuse beyond the RFC 8785 baseline.
/// Admit one JSON document and return its RFC 8785 canonical form.
///
/// Takes `&[u8]` rather than `&str` on purpose: a document arrives off a socket
/// or off disk as bytes, and the faults this crate refuses include ones no `&str`
/// can hold. A surrogate written directly in UTF-8 and an overlong form are
/// unrepresentable in a Rust string, so an entry point taking `&str` cannot be
/// handed them at all and its caller has to validate the bytes first -- which is
/// the check, moved back onto the caller. Measured 2026-09-19: the delegate's
/// only byte-facing entry point, `pipe`, takes `&str`.
///
/// # The number it hands back may not be the number it was given
///
/// RFC 8785 section 3.2.2.3 defers number formatting to ECMAScript, and
/// ECMAScript has one numeric type. So `admit(b"9007199254740993")` returns
/// `9007199254740992` and `admit(b"18446744073709551617")` returns
/// `18446744073709552000`: admitted, canonical, and a different number than the
/// wire carried. That is the specification working as written rather than a
/// defect in it -- the RFC's own reference vectors carry `1e30` and
/// `333333333.33333329`, so a default that refused inexact tokens would fail the
/// specification this crate implements.
///
/// A *producer* canonicalizing before it signs is the caller who cannot live with
/// that, because it signs the rewritten number. [`admit_ijson`] is the answer:
/// the RFC 7493 safe-integer profile refuses the token instead of rewriting it.
/// A verifier that hashes the bytes it received rather than bytes it
/// re-serialized is unaffected, which is why the profile is opt-in.
///
/// # Errors
///
/// Returns [`Error`] when the input is not one valid JSON value, repeats an
/// object member, carries a string that is not a sequence of Unicode scalar
/// values, nests past [`DEFAULT_MAX_DEPTH`], exceeds [`DEFAULT_MAX_BYTES`], or
/// carries a number with no finite double.
/// Admit one JSON document under the RFC 7493 I-JSON profile.
///
/// # Errors
///
/// As [`admit`], and additionally for an integer at or above 2^53 or a Unicode
/// noncharacter in a string literal.
/// Admit one JSON document under explicit options.
///
/// # Errors
///
/// As [`admit`], under the supplied [`Options`].
/// Whether `input` is admissible AND already its own canonical form.
///
/// The question a verifier has. It must hash the bytes it received, not
/// bytes it re-serialized, so what it needs to know is whether those two are the
/// same -- and a document that parses but was not stored canonically is exactly
/// the disagreement a second implementation exists to catch. Neither canonicalizer
/// on crates.io exposes this; a caller has to canonicalize and compare, and get
/// the comparison right.
/// Whether `input` is admissible and already canonical under `opts`.
///
/// [`is_canonical`] answers for RFC 8785 as written, which is the wrong question
/// for a caller who verifies under a profile: a document carrying a Unicode
/// noncharacter is canonical by RFC 8785 and refused by [`admit_ijson`], so a
/// verifier that admits bytes with the plain gate and then verifies them under
/// the profile has admitted a document its own profile will not accept. Ask with
/// the options the verify path uses.