Skip to main content

json_digest/
digest.rs

1use crate::error::{JsonDigestError, Result};
2use crate::hasher::{Blake3Cm, ContentHasher, Sha3Cj};
3use alloc::borrow::ToOwned;
4use alloc::format;
5use alloc::string::{String, ToString};
6use alloc::vec::Vec;
7use unicode_normalization::UnicodeNormalization;
8
9use crate::json_path;
10
11/// Returns an [NFKD normalized] unicode representation of the input
12///
13/// [NFKD normalized]: https://en.wikipedia.org/wiki/Unicode_equivalence#Normal_forms
14pub fn normalize_unicode(s: &str) -> String {
15    s.nfkd().collect()
16}
17
18/// Multibase-encoded hash of the utf8 representation of the provided string,
19/// prefixed with the hasher's two-character prefix.
20/// Character 'j' marks a JSON value hashed with SHA3, 'm' marks BLAKE3;
21/// 'c' stands for content hash.
22fn hash_str<H: ContentHasher>(content: &str) -> String {
23    format!("{}{}", H::PREFIX, H::hash(content.as_bytes()))
24}
25
26/// Action to take when walking a JSON tree node.
27#[derive(Clone, Copy, Debug, PartialEq)]
28enum WalkAction {
29    /// Keep the subtree expanded in the output.
30    Keep,
31    /// Replace the subtree with its content hash.
32    Hash,
33}
34
35/// Returns `true` when `path` starts with all elements of `prefix` (segment-wise).
36fn path_starts_with<P: AsRef<str>, Q: AsRef<str>>(path: &[P], prefix: &[Q]) -> bool {
37    path.len() >= prefix.len()
38        && path.iter().zip(prefix.iter()).all(|(a, b)| a.as_ref() == b.as_ref())
39}
40
41/// Splits a dotted JSON path (`.foo.bar`) into borrowed segments `["foo", "bar"]`.
42///
43/// Array index notation is expanded: `.foo[0].bar` → `["foo", "[0]", "bar"]`
44/// and `.foo[0][1]` → `["foo", "[0]", "[1]"]`.
45fn split_path(path: &str) -> Result<Vec<&str>> {
46    if !path.starts_with('.') {
47        return Err(JsonDigestError::InvalidPathStart { path: path.to_owned() });
48    }
49    let inner = &path[1..];
50    let mut segments = Vec::new();
51
52    for dot_seg in inner.split('.') {
53        let mut rest = dot_seg;
54        while let Some(bracket_pos) = rest.find('[') {
55            if bracket_pos > 0 {
56                segments.push(&rest[..bracket_pos]);
57            }
58            let close = rest[bracket_pos..]
59                .find(']')
60                .ok_or_else(|| JsonDigestError::UnclosedBracket { path: path.to_owned() })?;
61            segments.push(&rest[bracket_pos..bracket_pos + close + 1]);
62            rest = &rest[bracket_pos + close + 1..];
63        }
64        if !rest.is_empty() {
65            segments.push(rest);
66        }
67    }
68
69    Ok(segments)
70}
71
72/// Recursively walk a JSON value, producing a deterministic JSON string.
73///
74/// For every container node (object or array) the `decide` closure is called to
75/// determine whether that node should be kept open (canonical JSON) or replaced
76/// by its content hash.  Scalars are always rendered in canonical form.
77///
78/// The `path` argument tracks the current location using object-key strings and
79/// array-index `"[N]"` segments so that `decide` can make path-aware decisions.
80fn walk_json<H: ContentHasher>(
81    data: &serde_json::Value, path: &[String],
82    decide: &mut dyn FnMut(&[String], &serde_json::Value) -> WalkAction,
83) -> Result<String> {
84    let result = match data {
85        serde_json::Value::Array(arr) => {
86            let mut items = Vec::new();
87            for (i, item) in arr.iter().enumerate() {
88                let mut child_path = path.to_vec();
89                child_path.push(format!("[{i}]"));
90                items.push(walk_json::<H>(item, &child_path, decide)?);
91            }
92            format!("[{}]", items.join(","))
93        }
94
95        serde_json::Value::Object(obj) => {
96            let mut keys: Vec<_> = obj.keys().collect();
97            keys.sort();
98            let mut entries = Vec::new();
99            for key in keys {
100                if *key != normalize_unicode(key) {
101                    return Err(JsonDigestError::NonNormalizedInput);
102                }
103
104                let canonical_key = walk_json::<H>(
105                    &serde_json::Value::String(key.to_owned()),
106                    &[],
107                    &mut |_, _| WalkAction::Keep,
108                )?;
109                let mut child_path = path.to_vec();
110                child_path.push(key.clone());
111                let value = obj
112                    .get(key)
113                    .expect("keys() on serde_json::Map returned a key that is not in the map");
114                entries.push(format!(
115                    "{}:{}",
116                    canonical_key,
117                    walk_json::<H>(value, &child_path, decide)?
118                ));
119            }
120            format!("{{{}}}", entries.join(","))
121        }
122
123        serde_json::Value::Number(n) => canonicalize_number(n),
124
125        _ => {
126            let data_str = serde_json::to_string(data)
127                .expect("serde_json::to_string infallible for Null/Bool/String values");
128            normalize_unicode(&data_str)
129        }
130    };
131
132    if matches!(data, serde_json::Value::Array(_) | serde_json::Value::Object(_))
133        && decide(path, data) == WalkAction::Hash
134    {
135        Ok(format!("\"{}\"", hash_str::<H>(&result)))
136    } else {
137        Ok(result)
138    }
139}
140
141fn canonicalize_number(n: &serde_json::Number) -> String {
142    if let Some(i) = n.as_i64() {
143        return i.to_string();
144    }
145    if let Some(u) = n.as_u64() {
146        return u.to_string();
147    }
148    let f = n.as_f64().expect("serde_json::Number: i64/u64 rejected, must be f64");
149
150    // Integer-valued floats within i64 range → format as integer (3.0 → 3, 100.0 → 100).
151    // Upper bound is strict: `i64::MAX as f64` rounds up to 2^63, and casting
152    // 2^63 saturates to i64::MAX, which would collapse the distinct document
153    // 9223372036854775808.0 onto the integer 9223372036854775807.
154    if f.is_finite() && f >= (i64::MIN as f64) && f < (i64::MAX as f64) && (f as i64) as f64 == f {
155        return (f as i64).to_string();
156    }
157
158    let s = serde_json::to_string(n).expect("serde_json::Number::to_string infallible");
159
160    if let Some(e_pos) = s.find('e') {
161        let mantissa = &s[..e_pos];
162        let after_e = &s[e_pos + 1..];
163        let (sign, exp_str) = if let Some(stripped) = after_e.strip_prefix('+') {
164            ('+', stripped)
165        } else if let Some(stripped) = after_e.strip_prefix('-') {
166            ('-', stripped)
167        } else {
168            ('+', after_e) // no sign → positive exponent, add '+'
169        };
170        let exp: i64 = exp_str.parse().unwrap_or(0);
171
172        // ECMAScript Number::toString: decimal if 10⁻⁶ ≤ |f| < 10²¹
173        if (sign == '+' && exp < 21) || (sign == '-' && exp <= 6) {
174            return format!("{}", f);
175        }
176        // Strip trailing zeros from mantissa (handles raw-text "3.0e+30" → "3e+30")
177        let mantissa_stripped = mantissa.trim_end_matches('0');
178        let mantissa_clean = mantissa_stripped.strip_suffix('.').unwrap_or(mantissa_stripped);
179        return format!("{}e{}{}", mantissa_clean, sign, exp_str);
180    }
181
182    // No 'e' — format through f64 to strip trailing zeros (handles raw-text preservation
183    // for cases like "65.50" → "65.5", "0.001000" → "0.001", "1.2345678901234567890" → "1.2345678901234567")
184    format!("{}", f)
185}
186
187/// Constructs the deterministic string representation of the provided JSON value.
188///
189/// The same JSON document can be represented in multiple ways depending on
190/// property ordering and string encoding. This canonical JSON format lists
191/// property names in ascending order by their utf8-encoded byte arrays and
192/// uses the [NFKD normalized] unicode representation of all strings
193/// (both property names and string values).
194///
195/// The function will return error if a provided JSON object has properties
196/// with the same name in different unicode normalizations.
197/// Note that creating `Value` arguments, `serde_json` accepts objects
198/// having properties with exactly the same name and keeps only the last value,
199/// ignoring all previous ones.
200///
201/// [NFKD normalized]: https://en.wikipedia.org/wiki/Unicode_equivalence#Normal_forms
202pub fn canonical_json(data: &serde_json::Value) -> Result<String> {
203    canonical_json_with::<Sha3Cj>(data)
204}
205
206/// Canonical JSON with a specific content hasher (used internally).
207fn canonical_json_with<H: ContentHasher>(data: &serde_json::Value) -> Result<String> {
208    walk_json::<H>(data, &[], &mut |_, _| WalkAction::Keep)
209}
210
211/// Replace JSON (sub)tree(s) with their multibase-encoded [Merkle-root hash] strings.
212///
213/// Argument `keep_paths` can be created using function [`split_alternatives`] as needed.
214///
215/// Uses SHA3-256 (legacy `"cj"` prefix).  See [`mask_json_subtree_blake3`] for BLAKE3.
216///
217/// [`split_alternatives`]: ../json_path/fn.split_alternatives.html
218/// [`mask_json_subtree_blake3`]: fn.mask_json_subtree_blake3.html
219/// [Merkle-root hash]: https://en.wikipedia.org/wiki/Merkle_tree
220pub fn mask_json_subtree<'b>(
221    data: &serde_json::Value, keep_paths: impl AsRef<[&'b str]>,
222) -> Result<serde_json::Value> {
223    mask_json_subtree_with::<Sha3Cj>(data, keep_paths)
224}
225
226/// BLAKE3 variant of [`mask_json_subtree`].
227pub fn mask_json_subtree_blake3<'b>(
228    data: &serde_json::Value, keep_paths: impl AsRef<[&'b str]>,
229) -> Result<serde_json::Value> {
230    mask_json_subtree_with::<Blake3Cm>(data, keep_paths)
231}
232
233fn mask_json_subtree_with<'b, H: ContentHasher>(
234    data: &serde_json::Value, keep_paths: impl AsRef<[&'b str]>,
235) -> Result<serde_json::Value> {
236    let parsed: Vec<Vec<&'b str>> =
237        keep_paths.as_ref().iter().map(|p| split_path(p)).collect::<Result<_>>()?;
238
239    let mut decider = |path: &[String], _val: &serde_json::Value| -> WalkAction {
240        if parsed.is_empty() {
241            return WalkAction::Hash;
242        }
243        for kp in &parsed {
244            if path_starts_with(path, kp) || path_starts_with(kp, path) {
245                return WalkAction::Keep;
246            }
247        }
248        WalkAction::Hash
249    };
250
251    let result_str = walk_json::<H>(data, &[], &mut decider)?;
252    serde_json::from_str(&result_str).map_err(Into::into)
253}
254
255/// Convenience function to transform a JSON value into a (partially) masked JSON value.
256/// Only subtrees matching the provided JSON path pattern will be kept,
257/// all other subtrees will be masked.
258///
259/// Uses SHA3-256 (legacy `"cj"` prefix).  See [`selective_digest_json_blake3`].
260///
261/// [`selective_digest_json_blake3`]: fn.selective_digest_json_blake3.html
262pub fn selective_digest_json(
263    json_value: &serde_json::Value, keep_paths_str: &str,
264) -> Result<String> {
265    selective_digest_json_with::<Sha3Cj>(json_value, keep_paths_str)
266}
267
268/// BLAKE3 variant of [`selective_digest_json`].
269pub fn selective_digest_json_blake3(
270    json_value: &serde_json::Value, keep_paths_str: &str,
271) -> Result<String> {
272    selective_digest_json_with::<Blake3Cm>(json_value, keep_paths_str)
273}
274
275fn selective_digest_json_with<H: ContentHasher>(
276    json_value: &serde_json::Value, keep_paths_str: &str,
277) -> Result<String> {
278    let keep_paths_vec = json_path::split_alternatives(keep_paths_str);
279    let digest_json = match &json_value {
280        serde_json::Value::Object(_obj) => mask_json_subtree_with::<H>(json_value, keep_paths_vec),
281        serde_json::Value::Array(_arr) => mask_json_subtree_with::<H>(json_value, keep_paths_vec),
282        serde_json::Value::String(_s) => Ok(json_value.to_owned()),
283        _ => return Err(JsonDigestError::NonCompositeType),
284    }?;
285    match digest_json {
286        serde_json::Value::String(digest) => Ok(digest),
287        serde_json::Value::Object(_) => canonical_json_with::<H>(&digest_json),
288        _ => {
289            Err(JsonDigestError::Internal("digest should always return a string or object".into()))
290        }
291    }
292}
293
294/// Convenience function calling [`selective_digest_json`] with arbitrary serializable types.
295///
296/// Uses SHA3-256 (legacy `"cj"` prefix).  See [`selective_digest_data_blake3`].
297///
298/// [`selective_digest_json`]: ./fn.selective_digest_json.html
299/// [`selective_digest_data_blake3`]: fn.selective_digest_data_blake3.html
300pub fn selective_digest_data<T: serde::Serialize>(
301    data: &T, keep_paths_str: &str,
302) -> Result<String> {
303    selective_digest_data_with::<Sha3Cj, _>(data, keep_paths_str)
304}
305
306/// BLAKE3 variant of [`selective_digest_data`].
307pub fn selective_digest_data_blake3<T: serde::Serialize>(
308    data: &T, keep_paths_str: &str,
309) -> Result<String> {
310    selective_digest_data_with::<Blake3Cm, _>(data, keep_paths_str)
311}
312
313fn selective_digest_data_with<H: ContentHasher, T: serde::Serialize>(
314    data: &T, keep_paths_str: &str,
315) -> Result<String> {
316    let json_value = serde_json::to_value(data)?;
317    selective_digest_json_with::<H>(&json_value, keep_paths_str)
318}
319
320/// Convenience function calling [`selective_digest_json`] with a JSON string.
321///
322/// Uses SHA3-256 (legacy `"cj"` prefix).  See [`selective_digest_json_str_blake3`].
323///
324/// [`selective_digest_json`]: ./fn.selective_digest_json.html
325/// [`selective_digest_json_str_blake3`]: fn.selective_digest_json_str_blake3.html
326pub fn selective_digest_json_str(json_str: &str, keep_paths_str: &str) -> Result<String> {
327    selective_digest_json_str_with::<Sha3Cj>(json_str, keep_paths_str)
328}
329
330/// BLAKE3 variant of [`selective_digest_json_str`].
331pub fn selective_digest_json_str_blake3(json_str: &str, keep_paths_str: &str) -> Result<String> {
332    selective_digest_json_str_with::<Blake3Cm>(json_str, keep_paths_str)
333}
334
335fn selective_digest_json_str_with<H: ContentHasher>(
336    json_str: &str, keep_paths_str: &str,
337) -> Result<String> {
338    if json_str != normalize_unicode(json_str) {
339        return Err(JsonDigestError::NonNormalizedInput);
340    }
341
342    let json_value: serde_json::Value = serde_json::from_str(json_str)?;
343    selective_digest_json_with::<H>(&json_value, keep_paths_str)
344}
345
346const KEEP_NOTHING: &str = "";
347
348/// Convenience function for serializable types to mask the whole JSON tree into a digest, keep nothing.
349///
350/// Uses SHA3-256 (legacy `"cj"` prefix).  See [`digest_data_blake3`].
351///
352/// [`digest_data_blake3`]: fn.digest_data_blake3.html
353pub fn digest_data<T: serde::Serialize>(data: &T) -> Result<String> {
354    selective_digest_data_with::<Sha3Cj, _>(data, KEEP_NOTHING)
355}
356
357/// BLAKE3 variant of [`digest_data`].
358pub fn digest_data_blake3<T: serde::Serialize>(data: &T) -> Result<String> {
359    selective_digest_data_with::<Blake3Cm, _>(data, KEEP_NOTHING)
360}
361
362/// Convenience function for JSON strings to mask the whole JSON tree into a digest, keep nothing.
363///
364/// Uses SHA3-256 (legacy `"cj"` prefix).  See [`digest_json_str_blake3`].
365///
366/// [`digest_json_str_blake3`]: fn.digest_json_str_blake3.html
367pub fn digest_json_str(json_str: &str) -> Result<String> {
368    selective_digest_json_str_with::<Sha3Cj>(json_str, KEEP_NOTHING)
369}
370
371/// BLAKE3 variant of [`digest_json_str`].
372pub fn digest_json_str_blake3(json_str: &str) -> Result<String> {
373    selective_digest_json_str_with::<Blake3Cm>(json_str, KEEP_NOTHING)
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379    use hex::FromHex;
380    use serde::{Deserialize, Serialize};
381
382    #[derive(Clone, Debug, Deserialize, Serialize)]
383    struct TestData {
384        b: u32,
385        a: u32,
386    }
387
388    #[derive(Clone, Debug, Deserialize, Serialize)]
389    struct CompositeTestData<T> {
390        z: Option<T>,
391        y: Option<T>,
392    }
393
394    #[derive(Clone, Debug, Deserialize, Serialize)]
395    struct FlatData {
396        subject: String,
397        classification: String,
398    }
399
400    #[test]
401    fn blake3_digest_has_cm_prefix() -> Result<()> {
402        let obj = FlatData { subject: "Widget-123".into(), classification: "public".into() };
403        let result = digest_data_blake3(&obj)?;
404        assert!(result.starts_with("cm"), "expected cm prefix, got: {result}");
405        Ok(())
406    }
407
408    #[test]
409    fn float_i64_boundary_does_not_collapse_onto_i64_max() {
410        // 2^63 as f64 == `i64::MAX as f64` and saturating-casts to i64::MAX;
411        // it must take the float path, not the integer canonicalization.
412        let two_pow_63 = serde_json::json!(9223372036854775808.0);
413        let c = canonicalize_number(two_pow_63.as_number().unwrap());
414        assert_ne!(c, "9223372036854775807", "2^63.0 collapsed onto i64::MAX");
415        assert_eq!(c, "9223372036854776000");
416
417        // Largest integer-valued f64 below 2^63 still canonicalizes as integer.
418        let just_below = serde_json::json!(9223372036854774784.0);
419        assert_eq!(canonicalize_number(just_below.as_number().unwrap()), "9223372036854774784");
420
421        // The distinct integer 2^63-1 keeps its exact form.
422        let int_max = serde_json::json!(9223372036854775807i64);
423        assert_eq!(canonicalize_number(int_max.as_number().unwrap()), "9223372036854775807");
424    }
425
426    #[test]
427    fn reject_non_nfkd() -> Result<()> {
428        let key_nfc = String::from_utf8(Vec::from_hex("c3a16c6f6d").unwrap()).unwrap();
429        let key_nfkd = String::from_utf8(Vec::from_hex("61cc816c6f6d").unwrap()).unwrap();
430        assert_eq!(key_nfc, "álom");
431        assert_eq!(key_nfkd, "álom");
432
433        let str_nfc = format!("{{\"{key_nfc}\": 1}}");
434        let str_nfkd = format!("{{\"{key_nfkd}\": 1}}");
435        assert_eq!(digest_json_str(&str_nfkd)?, "cjuRab8yOeLzxmFY_fEMC79cW5z9XyihRhaGnTSvMabrA8");
436        assert!(digest_json_str(&str_nfc).is_err());
437
438        let json_value_nfc: serde_json::Value = serde_json::from_str(&str_nfc)?;
439        let json_value_nfkd: serde_json::Value = serde_json::from_str(&str_nfkd)?;
440        assert_eq!(
441            selective_digest_json(&json_value_nfkd, "")?,
442            "cjuRab8yOeLzxmFY_fEMC79cW5z9XyihRhaGnTSvMabrA8"
443        );
444        assert!(selective_digest_json(&json_value_nfc, "").is_err());
445        Ok(())
446    }
447
448    #[test]
449    fn digest_string_is_idempotent() {
450        let content_id = &r#""cjuzC-XxgzNMwYXtw8aMIAeS2Xjlw1hlSNKTvVtUwPuyYo""#;
451        let digest_id = digest_data(content_id).unwrap();
452        assert_eq!(content_id, &digest_id);
453    }
454
455    #[test]
456    fn test_json_digest() -> Result<()> {
457        let test_obj = TestData { b: 1, a: 2 };
458        {
459            let digested = digest_data(&test_obj)?;
460            assert_eq!(digested, "cjumTq1s6Tn6xkXolxHj4LmAo7DAb-zoPLhEa1BvpovAFU");
461        }
462        {
463            let digested = digest_data(&[&test_obj, &test_obj])?;
464            assert_eq!(digested, "cjuGkDpb1HL7F8xFKDFVj3felfKZzjrJy92-108uuPixNw");
465        }
466        {
467            let digested =
468                digest_data(&(&test_obj, "cjumTq1s6Tn6xkXolxHj4LmAo7DAb-zoPLhEa1BvpovAFU"))?;
469            assert_eq!(digested, "cjuGkDpb1HL7F8xFKDFVj3felfKZzjrJy92-108uuPixNw");
470        }
471        {
472            let digested = digest_data(&[
473                "cjumTq1s6Tn6xkXolxHj4LmAo7DAb-zoPLhEa1BvpovAFU",
474                "cjumTq1s6Tn6xkXolxHj4LmAo7DAb-zoPLhEa1BvpovAFU",
475            ])?;
476            assert_eq!(digested, "cjuGkDpb1HL7F8xFKDFVj3felfKZzjrJy92-108uuPixNw");
477        }
478        {
479            let x = &test_obj;
480            let comp = CompositeTestData { z: Some(x.clone()), y: Some(x.clone()) };
481            let digested = digest_data(&comp)?;
482            assert_eq!(digested, "cjubdcpA0FfHhD8yEpDzZ8vS5sm7yxkrX_wAJgmke2bWRQ");
483        }
484        {
485            let comp = CompositeTestData {
486                z: Some("cjumTq1s6Tn6xkXolxHj4LmAo7DAb-zoPLhEa1BvpovAFU".to_owned()),
487                y: Some("cjumTq1s6Tn6xkXolxHj4LmAo7DAb-zoPLhEa1BvpovAFU".to_owned()),
488            };
489            let digested = digest_data(&comp)?;
490            assert_eq!(digested, "cjubdcpA0FfHhD8yEpDzZ8vS5sm7yxkrX_wAJgmke2bWRQ");
491        }
492        Ok(())
493    }
494
495    #[test]
496    fn test_json_digest_blake3() -> Result<()> {
497        let test_obj = TestData { b: 1, a: 2 };
498        let digested = digest_data_blake3(&test_obj)?;
499        assert_eq!(digested, "cmursDCffzo2qmg4225E9FojsU5yn92SZfE3oufMQH2QYs");
500        Ok(())
501    }
502
503    #[test]
504    fn test_selective_digesting() -> Result<()> {
505        let test_obj = TestData { b: 1, a: 2 };
506        let x = &test_obj;
507        let composite = CompositeTestData { z: Some(x.clone()), y: Some(x.clone()) };
508        let double_complex =
509            CompositeTestData { z: Some(composite.clone()), y: Some(composite.clone()) };
510        let triple_complex =
511            CompositeTestData { z: Some(double_complex.clone()), y: Some(double_complex.clone()) };
512        {
513            let fully_digested = selective_digest_data(&composite, "")?;
514            assert_eq!(fully_digested, "cjubdcpA0FfHhD8yEpDzZ8vS5sm7yxkrX_wAJgmke2bWRQ");
515        }
516        {
517            let keep_y = selective_digest_data(&composite, ".y")?;
518            assert_eq!(
519                keep_y,
520                r#"{"y":{"a":2,"b":1},"z":"cjumTq1s6Tn6xkXolxHj4LmAo7DAb-zoPLhEa1BvpovAFU"}"#
521            );
522            let val: serde_json::Value = serde_json::from_str(&keep_y)?;
523            assert_eq!(digest_data(&val)?, "cjubdcpA0FfHhD8yEpDzZ8vS5sm7yxkrX_wAJgmke2bWRQ");
524        }
525        {
526            let keep_z = selective_digest_data(&composite, ".z")?;
527            assert_eq!(
528                keep_z,
529                r#"{"y":"cjumTq1s6Tn6xkXolxHj4LmAo7DAb-zoPLhEa1BvpovAFU","z":{"a":2,"b":1}}"#
530            );
531            let val: serde_json::Value = serde_json::from_str(&keep_z)?;
532            assert_eq!(digest_data(&val)?, "cjubdcpA0FfHhD8yEpDzZ8vS5sm7yxkrX_wAJgmke2bWRQ");
533        }
534        {
535            let digest = digest_data(&double_complex)?;
536            assert_eq!(digest, "cjuQLebyl_BJipFLibhWiStDBqK5J4JZq15ehUqybfTTKA");
537        }
538        {
539            let keep_yz = selective_digest_data(&double_complex, ".y.z")?;
540            assert_eq!(
541                keep_yz,
542                r#"{"y":{"y":"cjumTq1s6Tn6xkXolxHj4LmAo7DAb-zoPLhEa1BvpovAFU","z":{"a":2,"b":1}},"z":"cjubdcpA0FfHhD8yEpDzZ8vS5sm7yxkrX_wAJgmke2bWRQ"}"#
543            );
544            let val: serde_json::Value = serde_json::from_str(&keep_yz)?;
545            assert_eq!(digest_data(&val)?, "cjuQLebyl_BJipFLibhWiStDBqK5J4JZq15ehUqybfTTKA");
546        }
547        {
548            let digest = digest_data(&triple_complex)?;
549            assert_eq!(digest, "cjuik140L3w7LCi6z1eHt7Qgwr2X65-iy8HA6zqrlUdmVk");
550        }
551        {
552            let keep_yz = selective_digest_data(&triple_complex, ".y.y , .z.z")?;
553            assert_eq!(
554                keep_yz,
555                r#"{"y":{"y":{"y":{"a":2,"b":1},"z":{"a":2,"b":1}},"z":"cjubdcpA0FfHhD8yEpDzZ8vS5sm7yxkrX_wAJgmke2bWRQ"},"z":{"y":"cjubdcpA0FfHhD8yEpDzZ8vS5sm7yxkrX_wAJgmke2bWRQ","z":{"y":{"a":2,"b":1},"z":{"a":2,"b":1}}}}"#
556            );
557            let val: serde_json::Value = serde_json::from_str(&keep_yz)?;
558            assert_eq!(digest_data(&val)?, "cjuik140L3w7LCi6z1eHt7Qgwr2X65-iy8HA6zqrlUdmVk");
559        }
560        Ok(())
561    }
562
563    /// Load test vectors from the canonical test vector JSON file.
564    fn load_test_vectors() -> serde_json::Value {
565        let data = include_str!("../canonical-json-digest.json");
566        serde_json::from_str(data).expect("Failed to parse test vectors")
567    }
568
569    #[test]
570    fn test_canonical_blake3_vectors() -> Result<()> {
571        let vectors = load_test_vectors();
572        let test_cases =
573            vectors["blake3Vectors"].as_array().expect("blake3Vectors should be an array");
574
575        let mut failures = Vec::new();
576        for case in test_cases {
577            let id = case["id"].as_str().unwrap_or("<unknown>");
578            let expected_digest =
579                case["expected"]["digestHash"].as_str().expect("digestHash should be a string");
580
581            // We need the original input — look it up by id in the canonical vectors array.
582            let input = vectors["vectors"]
583                .as_array()
584                .unwrap()
585                .iter()
586                .find(|v| v["id"] == id)
587                .map(|v| &v["input"])
588                .expect("blake3 vector references unknown canonical id");
589
590            let digest = digest_data_blake3(input)?;
591            if digest != expected_digest {
592                failures.push(format!(
593                    "[{}] blake3 digest mismatch:\n  expected: {}\n  got:      {}",
594                    id, expected_digest, digest
595                ));
596            }
597        }
598
599        if !failures.is_empty() {
600            return Err(JsonDigestError::Internal(format!(
601                "BLAKE3 vector test failures:\n{}",
602                failures.join("\n\n")
603            )));
604        }
605        Ok(())
606    }
607
608    #[test]
609    fn test_canonical_selective_blake3_vectors() -> Result<()> {
610        let vectors = load_test_vectors();
611        let test_cases = vectors["blake3SelectiveVectors"]
612            .as_array()
613            .expect("blake3SelectiveVectors should be an array");
614
615        let mut failures = Vec::new();
616        for case in test_cases {
617            let id = case["id"].as_str().unwrap_or("<unknown>");
618            let keep_paths = case["keepPaths"].as_str().expect("keepPaths should be a string");
619            let expected_selective = case["expected"]["selectiveDigest"]
620                .as_str()
621                .expect("selectiveDigest should be a string");
622            let expected_redigest =
623                case["expected"]["redigestHash"].as_str().expect("redigestHash should be a string");
624
625            // Look up the input in the original selective vectors array.
626            let input = vectors["selectiveVectors"]
627                .as_array()
628                .unwrap()
629                .iter()
630                .find(|v| v["id"] == id)
631                .map(|v| &v["input"])
632                .expect("blake3 selective vector references unknown id");
633
634            let selective = selective_digest_data_blake3(input, keep_paths)?;
635            if selective != expected_selective {
636                failures.push(format!(
637                    "[{}] blake3 selectiveDigest mismatch:\n  expected: {}\n  got:      {}",
638                    id, expected_selective, selective
639                ));
640            }
641
642            let redigest =
643                digest_data_blake3(&serde_json::from_str::<serde_json::Value>(&selective)?)?;
644            if redigest != expected_redigest {
645                failures.push(format!(
646                    "[{}] blake3 redigest mismatch:\n  expected: {}\n  got:      {}",
647                    id, expected_redigest, redigest
648                ));
649            }
650        }
651
652        if !failures.is_empty() {
653            return Err(JsonDigestError::Internal(format!(
654                "BLAKE3 selective vector test failures:\n{}",
655                failures.join("\n\n")
656            )));
657        }
658        Ok(())
659    }
660
661    #[test]
662    fn test_canonical_vectors() -> Result<()> {
663        let vectors = load_test_vectors();
664        let test_cases = vectors["vectors"].as_array().expect("vectors should be an array");
665
666        let mut failures = Vec::new();
667        for case in test_cases {
668            let id = case["id"].as_str().unwrap_or("<unknown>");
669            let input = &case["input"];
670            let expected_digest =
671                case["expected"]["digestHash"].as_str().expect("digestHash should be a string");
672
673            // If canonical form is specified, check it
674            if let Some(expected_canonical) = case["expected"]["canonical"].as_str() {
675                let canonical = canonical_json(input)?;
676                if canonical != expected_canonical {
677                    failures.push(format!(
678                        "[{}] canonical_json mismatch:\n  expected: {}\n  got:      {}",
679                        id, expected_canonical, canonical
680                    ));
681                }
682            }
683
684            // Check digest
685            let digest = digest_data(input)?;
686            if digest != expected_digest {
687                failures.push(format!(
688                    "[{}] digest mismatch:\n  expected: {}\n  got:      {}",
689                    id, expected_digest, digest
690                ));
691            }
692        }
693
694        if !failures.is_empty() {
695            return Err(JsonDigestError::Internal(format!(
696                "Vector test failures:\n{}",
697                failures.join("\n\n")
698            )));
699        }
700        Ok(())
701    }
702
703    #[test]
704    fn test_canonical_selective_vectors() -> Result<()> {
705        let vectors = load_test_vectors();
706        let test_cases =
707            vectors["selectiveVectors"].as_array().expect("selectiveVectors should be an array");
708
709        let mut failures = Vec::new();
710        for case in test_cases {
711            let id = case["id"].as_str().unwrap_or("<unknown>");
712            let input = &case["input"];
713            let keep_paths = case["keepPaths"].as_str().expect("keepPaths should be a string");
714            let expected_selective = case["expected"]["selectiveDigest"]
715                .as_str()
716                .expect("selectiveDigest should be a string");
717            let expected_redigest =
718                case["expected"]["redigestHash"].as_str().expect("redigestHash should be a string");
719
720            let selective = selective_digest_data(input, keep_paths)?;
721            if selective != expected_selective {
722                failures.push(format!(
723                    "[{}] selectiveDigest mismatch:\n  expected: {}\n  got:      {}",
724                    id, expected_selective, selective
725                ));
726            }
727
728            let redigest = digest_data(&serde_json::from_str::<serde_json::Value>(&selective)?)?;
729            if redigest != expected_redigest {
730                failures.push(format!(
731                    "[{}] redigest mismatch:\n  expected: {}\n  got:      {}",
732                    id, expected_redigest, redigest
733                ));
734            }
735        }
736
737        if !failures.is_empty() {
738            return Err(JsonDigestError::Internal(format!(
739                "Selective vector test failures:\n{}",
740                failures.join("\n\n")
741            )));
742        }
743        Ok(())
744    }
745}