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
11pub fn normalize_unicode(s: &str) -> String {
15 s.nfkd().collect()
16}
17
18fn hash_str<H: ContentHasher>(content: &str) -> String {
23 format!("{}{}", H::PREFIX, H::hash(content.as_bytes()))
24}
25
26#[derive(Clone, Copy, Debug, PartialEq)]
28enum WalkAction {
29 Keep,
31 Hash,
33}
34
35fn 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
41fn 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
72fn 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 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) };
170 let exp: i64 = exp_str.parse().unwrap_or(0);
171
172 if (sign == '+' && exp < 21) || (sign == '-' && exp <= 6) {
174 return format!("{}", f);
175 }
176 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 format!("{}", f)
185}
186
187pub fn canonical_json(data: &serde_json::Value) -> Result<String> {
203 canonical_json_with::<Sha3Cj>(data)
204}
205
206fn canonical_json_with<H: ContentHasher>(data: &serde_json::Value) -> Result<String> {
208 walk_json::<H>(data, &[], &mut |_, _| WalkAction::Keep)
209}
210
211pub 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
226pub 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
255pub 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
268pub 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
294pub 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
306pub 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
320pub 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
330pub 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
348pub fn digest_data<T: serde::Serialize>(data: &T) -> Result<String> {
354 selective_digest_data_with::<Sha3Cj, _>(data, KEEP_NOTHING)
355}
356
357pub fn digest_data_blake3<T: serde::Serialize>(data: &T) -> Result<String> {
359 selective_digest_data_with::<Blake3Cm, _>(data, KEEP_NOTHING)
360}
361
362pub fn digest_json_str(json_str: &str) -> Result<String> {
368 selective_digest_json_str_with::<Sha3Cj>(json_str, KEEP_NOTHING)
369}
370
371pub 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 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 let just_below = serde_json::json!(9223372036854774784.0);
419 assert_eq!(canonicalize_number(just_below.as_number().unwrap()), "9223372036854774784");
420
421 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 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 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 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 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 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}