Skip to main content

codec_jcs/
canonicalize.rs

1// SPDX-FileCopyrightText: 2026 ReallyMe LLC
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5use std::cmp::Ordering;
6
7use serde_json::Value;
8use zeroize::Zeroizing;
9
10use crate::error::JcsError;
11use crate::parse_json::{parse_json_text, SensitiveJsonValue, SensitiveNumber};
12
13const MAX_INTEROPERABLE_INTEGER: u64 = 9_007_199_254_740_991;
14const MIN_INTEROPERABLE_INTEGER: i64 = -9_007_199_254_740_991;
15const MAX_INTEROPERABLE_INTEGER_F64: f64 = 9_007_199_254_740_991.0;
16const MIN_INTEROPERABLE_INTEGER_F64: f64 = -9_007_199_254_740_991.0;
17
18/// Canonicalize a trusted, already-materialized JSON value according to RFC
19/// 8785 (JSON Canonicalization Scheme).
20///
21/// Integer and integer-valued binary64 numbers are rejected outside the
22/// interoperable `[-(2^53)+1, (2^53)-1]` range. This function cannot determine
23/// whether a source document contained duplicate object member names because
24/// [`Value`] has already discarded that provenance. It is therefore intended
25/// only for values constructed programmatically or produced by a parser that
26/// already enforced the same duplicate-member policy. Call
27/// [`canonicalize_json_text`] at every untrusted text boundary.
28pub fn canonicalize_trusted_json_value(value: &Value) -> Result<String, JcsError> {
29    let output_length = canonicalized_len(value, 0)?;
30    let mut output = Zeroizing::new(String::with_capacity(output_length));
31    write_canonical(value, 0, &mut output)?;
32    if output.len() != output_length {
33        return Err(JcsError::SerializationError);
34    }
35    Ok(core::mem::take(&mut *output))
36}
37
38/// Parse and canonicalize one untrusted JSON text according to RFC 8785.
39///
40/// Unlike deserializing directly into [`Value`], this entry point detects and
41/// rejects duplicate object member names instead of silently retaining one
42/// value. It also rejects trailing data. Integer tokens retained exactly as
43/// `i64`, `u64`, or integer-valued binary64 are rejected outside the
44/// interoperable range, while non-integer binary64 numbers follow RFC 8785's
45/// required ECMAScript rounding behavior.
46pub fn canonicalize_json_text(input: &str) -> Result<String, JcsError> {
47    let value = parse_json_text(input)?;
48    canonicalize_sensitive_json_value(&value)
49}
50
51/// `depth` counts the array/object containers currently open, bounded by
52/// [`MAX_NESTING_DEPTH`](crate::MAX_NESTING_DEPTH) as defense in depth
53/// against a `Value` that was built without a parser depth limit.
54fn write_canonical(value: &Value, depth: usize, output: &mut String) -> Result<(), JcsError> {
55    match value {
56        Value::Null => output.push_str("null"),
57        Value::Bool(false) => output.push_str("false"),
58        Value::Bool(true) => output.push_str("true"),
59        Value::Number(value) => write_canonical_number(value, output)?,
60        Value::String(value) => write_escaped_string(value, output),
61        Value::Array(values) => write_canonical_array(values, depth, output)?,
62        Value::Object(values) => {
63            let child_depth = descend(depth)?;
64            let mut keys: Vec<&String> = values.keys().collect();
65            // RFC 8785 §3.2.3: sort by UTF-16 code unit, NOT by Unicode
66            // scalar value. The two orders agree across the BMP but diverge
67            // for supplementary-plane names, whose UTF-16 surrogate code
68            // units (0xD800–0xDFFF) sort below BMP code points such as
69            // 0xE000–0xFFFF. A plain `str` sort (UTF-8 / code-point order)
70            // would place them differently and yield non-canonical output.
71            keys.sort_by(|left, right| utf16_cmp(left, right));
72
73            output.push('{');
74            for (index, key) in keys.iter().enumerate() {
75                if index > 0 {
76                    output.push(',');
77                }
78                write_escaped_string(key, output);
79                output.push(':');
80                write_canonical(&values[*key], child_depth, output)?;
81            }
82            output.push('}');
83        }
84    }
85    Ok(())
86}
87
88/// Enters one nesting level, rejecting input deeper than
89/// [`MAX_NESTING_DEPTH`](crate::MAX_NESTING_DEPTH).
90fn descend(depth: usize) -> Result<usize, JcsError> {
91    let next = depth.checked_add(1).ok_or(JcsError::DepthExceeded)?;
92    if next > crate::MAX_NESTING_DEPTH {
93        return Err(JcsError::DepthExceeded);
94    }
95    Ok(next)
96}
97
98/// Compares two strings by their UTF-16 code units, as RFC 8785 requires
99/// for object member ordering.
100fn utf16_cmp(left: &str, right: &str) -> Ordering {
101    left.encode_utf16().cmp(right.encode_utf16())
102}
103
104fn write_canonical_number(value: &serde_json::Number, output: &mut String) -> Result<(), JcsError> {
105    if let Some(unsigned) = value.as_u64() {
106        if unsigned > MAX_INTEROPERABLE_INTEGER {
107            return Err(JcsError::IntegerOutsideInteroperableRange);
108        }
109        let mut buffer = itoa::Buffer::new();
110        output.push_str(buffer.format(unsigned));
111        return Ok(());
112    }
113    if let Some(signed) = value.as_i64() {
114        if signed < MIN_INTEROPERABLE_INTEGER {
115            return Err(JcsError::IntegerOutsideInteroperableRange);
116        }
117        let mut buffer = itoa::Buffer::new();
118        output.push_str(buffer.format(signed));
119        return Ok(());
120    }
121
122    let float = value.as_f64().ok_or(JcsError::SerializationError)?;
123    if !float.is_finite() {
124        return Err(JcsError::NonFiniteNumber);
125    }
126    validate_interoperable_float_integer(float)?;
127
128    // RFC 8785 §3.2.2.3 mandates the ECMAScript `Number.prototype.toString`
129    // algorithm (exponent thresholds, exponent signs, shortest round-trip
130    // digits). `serde_json`/`ryu` do not fully match those ES6 formatting
131    // rules, so we use `ryu-js` after applying ReallyMe's stricter
132    // interoperable-integer policy.
133    let mut buffer = ryu_js::Buffer::new();
134    output.push_str(buffer.format_finite(float));
135    Ok(())
136}
137
138fn write_sensitive_number(value: &SensitiveNumber, output: &mut String) -> Result<(), JcsError> {
139    match value {
140        SensitiveNumber::Unsigned(unsigned) => {
141            if *unsigned > MAX_INTEROPERABLE_INTEGER {
142                return Err(JcsError::IntegerOutsideInteroperableRange);
143            }
144            let mut buffer = itoa::Buffer::new();
145            output.push_str(buffer.format(*unsigned));
146        }
147        SensitiveNumber::Signed(signed) => {
148            if *signed < MIN_INTEROPERABLE_INTEGER {
149                return Err(JcsError::IntegerOutsideInteroperableRange);
150            }
151            let mut buffer = itoa::Buffer::new();
152            output.push_str(buffer.format(*signed));
153        }
154        SensitiveNumber::Float(float) => {
155            if !float.is_finite() {
156                return Err(JcsError::NonFiniteNumber);
157            }
158            validate_interoperable_float_integer(*float)?;
159            let mut buffer = ryu_js::Buffer::new();
160            output.push_str(buffer.format_finite(*float));
161        }
162    }
163    Ok(())
164}
165
166fn write_canonical_array(
167    values: &[Value],
168    depth: usize,
169    output: &mut String,
170) -> Result<(), JcsError> {
171    let child_depth = descend(depth)?;
172    output.push('[');
173    for (index, item) in values.iter().enumerate() {
174        if index > 0 {
175            output.push(',');
176        }
177        write_canonical(item, child_depth, output)?;
178    }
179    output.push(']');
180    Ok(())
181}
182
183fn canonicalized_len(value: &Value, depth: usize) -> Result<usize, JcsError> {
184    match value {
185        Value::Null => Ok("null".len()),
186        Value::Bool(false) => Ok("false".len()),
187        Value::Bool(true) => Ok("true".len()),
188        Value::Number(value) => canonical_number_len(value),
189        Value::String(value) => escaped_string_len(value),
190        Value::Array(values) => canonical_array_len(values, depth),
191        Value::Object(values) => canonical_object_len(values, depth),
192    }
193}
194
195fn canonicalize_sensitive_json_value(value: &SensitiveJsonValue) -> Result<String, JcsError> {
196    let output_length = canonicalized_sensitive_len(value, 0)?;
197    let mut output = Zeroizing::new(String::with_capacity(output_length));
198    write_sensitive_canonical(value, 0, &mut output)?;
199    if output.len() != output_length {
200        return Err(JcsError::SerializationError);
201    }
202    Ok(core::mem::take(&mut *output))
203}
204
205fn write_sensitive_canonical(
206    value: &SensitiveJsonValue,
207    depth: usize,
208    output: &mut String,
209) -> Result<(), JcsError> {
210    match value {
211        SensitiveJsonValue::Null => output.push_str("null"),
212        SensitiveJsonValue::Bool(false) => output.push_str("false"),
213        SensitiveJsonValue::Bool(true) => output.push_str("true"),
214        SensitiveJsonValue::Number(value) => write_sensitive_number(value, output)?,
215        SensitiveJsonValue::String(value) => write_escaped_string(value, output),
216        SensitiveJsonValue::Array(values) => {
217            let child_depth = descend(depth)?;
218            output.push('[');
219            for (index, item) in values.iter().enumerate() {
220                if index > 0 {
221                    output.push(',');
222                }
223                write_sensitive_canonical(item, child_depth, output)?;
224            }
225            output.push(']');
226        }
227        SensitiveJsonValue::Object(values) => {
228            let child_depth = descend(depth)?;
229            let mut keys: Vec<&String> = values.keys().collect();
230            keys.sort_by(|left, right| utf16_cmp(left, right));
231
232            output.push('{');
233            for (index, key) in keys.iter().enumerate() {
234                if index > 0 {
235                    output.push(',');
236                }
237                write_escaped_string(key, output);
238                output.push(':');
239                let value = values.get(*key).ok_or(JcsError::SerializationError)?;
240                write_sensitive_canonical(value, child_depth, output)?;
241            }
242            output.push('}');
243        }
244    }
245    Ok(())
246}
247
248fn canonicalized_sensitive_len(
249    value: &SensitiveJsonValue,
250    depth: usize,
251) -> Result<usize, JcsError> {
252    match value {
253        SensitiveJsonValue::Null => Ok("null".len()),
254        SensitiveJsonValue::Bool(false) => Ok("false".len()),
255        SensitiveJsonValue::Bool(true) => Ok("true".len()),
256        SensitiveJsonValue::Number(value) => canonical_sensitive_number_len(value),
257        SensitiveJsonValue::String(value) => escaped_string_len(value),
258        SensitiveJsonValue::Array(values) => canonical_sensitive_array_len(values, depth),
259        SensitiveJsonValue::Object(values) => canonical_sensitive_object_len(values, depth),
260    }
261}
262
263fn canonical_sensitive_array_len(
264    values: &[Box<SensitiveJsonValue>],
265    depth: usize,
266) -> Result<usize, JcsError> {
267    let child_depth = descend(depth)?;
268    let mut length = "["
269        .len()
270        .checked_add("]".len())
271        .ok_or(JcsError::SerializationError)?;
272    for (index, item) in values.iter().enumerate() {
273        if index > 0 {
274            length = length
275                .checked_add(",".len())
276                .ok_or(JcsError::SerializationError)?;
277        }
278        length = length
279            .checked_add(canonicalized_sensitive_len(item, child_depth)?)
280            .ok_or(JcsError::SerializationError)?;
281    }
282    Ok(length)
283}
284
285fn canonical_sensitive_object_len(
286    values: &std::collections::BTreeMap<String, Box<SensitiveJsonValue>>,
287    depth: usize,
288) -> Result<usize, JcsError> {
289    let child_depth = descend(depth)?;
290    let mut keys: Vec<&String> = values.keys().collect();
291    keys.sort_by(|left, right| utf16_cmp(left, right));
292
293    let mut length = "{"
294        .len()
295        .checked_add("}".len())
296        .ok_or(JcsError::SerializationError)?;
297    for (index, key) in keys.iter().enumerate() {
298        if index > 0 {
299            length = length
300                .checked_add(",".len())
301                .ok_or(JcsError::SerializationError)?;
302        }
303        let value = values.get(*key).ok_or(JcsError::SerializationError)?;
304        let value_length = canonicalized_sensitive_len(value, child_depth)?;
305        length = length
306            .checked_add(escaped_string_len(key)?)
307            .and_then(|value| value.checked_add(":".len()))
308            .and_then(|value| value.checked_add(value_length))
309            .ok_or(JcsError::SerializationError)?;
310    }
311    Ok(length)
312}
313
314fn canonical_array_len(values: &[Value], depth: usize) -> Result<usize, JcsError> {
315    let child_depth = descend(depth)?;
316    let mut length = "["
317        .len()
318        .checked_add("]".len())
319        .ok_or(JcsError::SerializationError)?;
320    for (index, item) in values.iter().enumerate() {
321        if index > 0 {
322            length = length
323                .checked_add(",".len())
324                .ok_or(JcsError::SerializationError)?;
325        }
326        length = length
327            .checked_add(canonicalized_len(item, child_depth)?)
328            .ok_or(JcsError::SerializationError)?;
329    }
330    Ok(length)
331}
332
333fn canonical_object_len(
334    values: &serde_json::Map<String, Value>,
335    depth: usize,
336) -> Result<usize, JcsError> {
337    let child_depth = descend(depth)?;
338    let mut keys: Vec<&String> = values.keys().collect();
339    keys.sort_by(|left, right| utf16_cmp(left, right));
340
341    let mut length = "{"
342        .len()
343        .checked_add("}".len())
344        .ok_or(JcsError::SerializationError)?;
345    for (index, key) in keys.iter().enumerate() {
346        if index > 0 {
347            length = length
348                .checked_add(",".len())
349                .ok_or(JcsError::SerializationError)?;
350        }
351        let value_length = canonicalized_len(&values[*key], child_depth)?;
352        length = length
353            .checked_add(escaped_string_len(key)?)
354            .and_then(|value| value.checked_add(":".len()))
355            .and_then(|value| value.checked_add(value_length))
356            .ok_or(JcsError::SerializationError)?;
357    }
358    Ok(length)
359}
360
361fn canonical_number_len(value: &serde_json::Number) -> Result<usize, JcsError> {
362    if let Some(unsigned) = value.as_u64() {
363        if unsigned > MAX_INTEROPERABLE_INTEGER {
364            return Err(JcsError::IntegerOutsideInteroperableRange);
365        }
366        let mut buffer = itoa::Buffer::new();
367        return Ok(buffer.format(unsigned).len());
368    }
369    if let Some(signed) = value.as_i64() {
370        if signed < MIN_INTEROPERABLE_INTEGER {
371            return Err(JcsError::IntegerOutsideInteroperableRange);
372        }
373        let mut buffer = itoa::Buffer::new();
374        return Ok(buffer.format(signed).len());
375    }
376
377    let float = value.as_f64().ok_or(JcsError::SerializationError)?;
378    if !float.is_finite() {
379        return Err(JcsError::NonFiniteNumber);
380    }
381    validate_interoperable_float_integer(float)?;
382
383    let mut buffer = ryu_js::Buffer::new();
384    Ok(buffer.format_finite(float).len())
385}
386
387fn canonical_sensitive_number_len(value: &SensitiveNumber) -> Result<usize, JcsError> {
388    match value {
389        SensitiveNumber::Unsigned(unsigned) => {
390            if *unsigned > MAX_INTEROPERABLE_INTEGER {
391                return Err(JcsError::IntegerOutsideInteroperableRange);
392            }
393            let mut buffer = itoa::Buffer::new();
394            Ok(buffer.format(*unsigned).len())
395        }
396        SensitiveNumber::Signed(signed) => {
397            if *signed < MIN_INTEROPERABLE_INTEGER {
398                return Err(JcsError::IntegerOutsideInteroperableRange);
399            }
400            let mut buffer = itoa::Buffer::new();
401            Ok(buffer.format(*signed).len())
402        }
403        SensitiveNumber::Float(float) => {
404            if !float.is_finite() {
405                return Err(JcsError::NonFiniteNumber);
406            }
407            validate_interoperable_float_integer(*float)?;
408            let mut buffer = ryu_js::Buffer::new();
409            Ok(buffer.format_finite(*float).len())
410        }
411    }
412}
413
414fn validate_interoperable_float_integer(value: f64) -> Result<(), JcsError> {
415    if value.fract() == 0.0
416        && !(MIN_INTEROPERABLE_INTEGER_F64..=MAX_INTEROPERABLE_INTEGER_F64).contains(&value)
417    {
418        return Err(JcsError::IntegerOutsideInteroperableRange);
419    }
420    Ok(())
421}
422
423fn escaped_string_len(value: &str) -> Result<usize, JcsError> {
424    let mut length = "\""
425        .len()
426        .checked_add("\"".len())
427        .ok_or(JcsError::SerializationError)?;
428    for character in value.chars() {
429        let scalar = u32::from(character);
430        if (0xfdd0..=0xfdef).contains(&scalar) || scalar & 0xfffe == 0xfffe {
431            return Err(JcsError::Noncharacter);
432        }
433        length = length
434            .checked_add(escaped_character_len(character))
435            .ok_or(JcsError::SerializationError)?;
436    }
437    Ok(length)
438}
439
440fn escaped_character_len(character: char) -> usize {
441    match character {
442        '"' | '\\' | '\u{08}' | '\t' | '\n' | '\u{0c}' | '\r' => 2,
443        '\u{00}'..='\u{1f}' => 6,
444        _ => character.len_utf8(),
445    }
446}
447
448fn write_escaped_string(value: &str, output: &mut String) {
449    const HEX: &[u8; 16] = b"0123456789abcdef";
450
451    output.push('"');
452    for character in value.chars() {
453        match character {
454            '"' => output.push_str("\\\""),
455            '\\' => output.push_str("\\\\"),
456            '\u{08}' => output.push_str("\\b"),
457            '\t' => output.push_str("\\t"),
458            '\n' => output.push_str("\\n"),
459            '\u{0c}' => output.push_str("\\f"),
460            '\r' => output.push_str("\\r"),
461            '\u{00}'..='\u{1f}' => {
462                let byte = character as u8;
463                output.push_str("\\u00");
464                output.push(char::from(HEX[usize::from(byte >> 4)]));
465                output.push(char::from(HEX[usize::from(byte & 0x0f)]));
466            }
467            _ => output.push(character),
468        }
469    }
470    output.push('"');
471}