1use std::collections::{HashMap, HashSet};
11use std::fmt::Write as _;
12
13use serde::de::DeserializeOwned;
14use serde::Serialize;
15use sha2::{Digest, Sha256};
16use thiserror::Error;
17use unicode_normalization::UnicodeNormalization;
18
19use crate::error::{DagMlError, Result as DagMlResult};
20
21pub const TCV1_PREFIX: &[u8] = b"DAGML-TCV1\0";
23
24pub const TCV1_UNICODE_VERSION: (u8, u8, u8) = (17, 0, 0);
26
27const _: () = assert!(unicode_normalization::UNICODE_VERSION.0 == TCV1_UNICODE_VERSION.0);
28const _: () = assert!(unicode_normalization::UNICODE_VERSION.1 == TCV1_UNICODE_VERSION.1);
29const _: () = assert!(unicode_normalization::UNICODE_VERSION.2 == TCV1_UNICODE_VERSION.2);
30
31const MAX_NESTING_DEPTH: usize = 128;
32
33#[derive(Clone, Debug, Eq, PartialEq)]
41pub enum CanonicalInteger {
42 Signed(i64),
43 Unsigned(u64),
44}
45
46impl CanonicalInteger {
47 fn decimal(&self) -> String {
48 match self {
49 Self::Signed(value) => value.to_string(),
50 Self::Unsigned(value) => value.to_string(),
51 }
52 }
53}
54
55#[derive(Clone, Debug, PartialEq)]
65pub enum TypedCanonicalValue {
66 Null,
67 Bool(bool),
68 Integer(CanonicalInteger),
69 Binary64(f64),
70 String(String),
71 Array(Vec<TypedCanonicalValue>),
72 Object(Vec<(String, TypedCanonicalValue)>),
73}
74
75impl TypedCanonicalValue {
76 pub fn fingerprint(&self) -> Result<String, Tcv1Error> {
78 tcv1_sha256(self)
79 }
80
81 pub fn fingerprint_without(&self, key: &str) -> Result<String, Tcv1Error> {
87 let Self::Object(entries) = self else {
88 return Err(Tcv1Error::ExpectedObject);
89 };
90 let normalized_key = normalize(key);
91 let mut removed = 0_usize;
92 let mut filtered = Vec::with_capacity(entries.len().saturating_sub(1));
93 for (member_key, member_value) in entries {
94 if normalize(member_key) == normalized_key {
95 removed += 1;
96 } else {
97 filtered.push((member_key.clone(), member_value.clone()));
98 }
99 }
100 match removed {
101 0 => Err(Tcv1Error::MissingObjectKey(normalized_key)),
102 1 => tcv1_sha256(&Self::Object(filtered)),
103 _ => Err(Tcv1Error::AmbiguousObjectKey(normalized_key)),
104 }
105 }
106}
107
108#[derive(Clone, Debug, Eq, PartialEq, Error)]
110pub enum Tcv1Error {
111 #[error("input is not valid UTF-8 at byte {valid_up_to}")]
112 InvalidUtf8 {
113 valid_up_to: usize,
114 error_len: Option<usize>,
115 },
116
117 #[error("invalid JSON at UTF-8 byte {offset}: {message}")]
118 InvalidJson {
119 offset: usize,
120 message: &'static str,
121 },
122
123 #[error("duplicate JSON object key `{key}` at UTF-8 byte {offset}")]
124 DuplicateObjectKey { key: String, offset: usize },
125
126 #[error("NFC-colliding JSON object keys `{first}` and `{second}` at UTF-8 byte {offset}")]
127 NfcKeyCollision {
128 first: String,
129 second: String,
130 offset: usize,
131 },
132
133 #[error("integer token at UTF-8 byte {offset} is outside the TCV1 {domain} range")]
134 IntegerOutOfRange { offset: usize, domain: &'static str },
135
136 #[error("number token at UTF-8 byte {offset} is outside finite binary64 range")]
137 Binary64OutOfRange { offset: usize },
138
139 #[error("TCV1 value nesting exceeds {MAX_NESTING_DEPTH} levels")]
140 NestingTooDeep,
141
142 #[error("TCV1 collection length does not fit u64")]
143 LengthOverflow,
144
145 #[error("programmatically constructed TCV1 binary64 must be finite")]
146 NonFiniteBinary64,
147
148 #[error("fingerprint_without requires a TCV1 object")]
149 ExpectedObject,
150
151 #[error("object does not contain normalized key `{0}`")]
152 MissingObjectKey(String),
153
154 #[error("object contains more than one key normalized as `{0}`")]
155 AmbiguousObjectKey(String),
156}
157
158pub fn parse_typed_json(input: &str) -> Result<TypedCanonicalValue, Tcv1Error> {
164 let mut parser = Parser::new(input);
165 let value = parser.parse_value(0)?;
166 parser.skip_whitespace();
167 if parser.offset != input.len() {
168 return Err(parser.invalid("trailing data after the JSON value"));
169 }
170 Ok(value)
171}
172
173pub fn parse_typed_json_bytes(input: &[u8]) -> Result<TypedCanonicalValue, Tcv1Error> {
175 let input = std::str::from_utf8(input).map_err(|error| Tcv1Error::InvalidUtf8 {
176 valid_up_to: error.valid_up_to(),
177 error_len: error.error_len(),
178 })?;
179 parse_typed_json(input)
180}
181
182pub fn validate_typed_serde_value(value: &serde_json::Value) -> Result<(), Tcv1Error> {
190 fn convert(value: &serde_json::Value) -> TypedCanonicalValue {
191 match value {
192 serde_json::Value::Null => TypedCanonicalValue::Null,
193 serde_json::Value::Bool(value) => TypedCanonicalValue::Bool(*value),
194 serde_json::Value::Number(value) => {
195 if let Some(value) = value.as_u64() {
196 TypedCanonicalValue::Integer(CanonicalInteger::Unsigned(value))
197 } else if let Some(value) = value.as_i64() {
198 TypedCanonicalValue::Integer(CanonicalInteger::Signed(value))
199 } else {
200 TypedCanonicalValue::Binary64(
201 value
202 .as_f64()
203 .expect("serde_json numbers are integer or finite binary64"),
204 )
205 }
206 }
207 serde_json::Value::String(value) => TypedCanonicalValue::String(value.clone()),
208 serde_json::Value::Array(values) => {
209 TypedCanonicalValue::Array(values.iter().map(convert).collect())
210 }
211 serde_json::Value::Object(values) => TypedCanonicalValue::Object(
212 values
213 .iter()
214 .map(|(key, value)| (key.clone(), convert(value)))
215 .collect(),
216 ),
217 }
218 }
219
220 tcv1_encode(&convert(value)).map(|_| ())
221}
222
223pub fn tcv1_encode(value: &TypedCanonicalValue) -> Result<Vec<u8>, Tcv1Error> {
228 let mut output = Vec::new();
229 encode_value(value, &mut output, 0)?;
230 Ok(output)
231}
232
233pub fn tcv1_preimage(value: &TypedCanonicalValue) -> Result<Vec<u8>, Tcv1Error> {
235 let mut output = Vec::from(TCV1_PREFIX);
236 encode_value(value, &mut output, 0)?;
237 Ok(output)
238}
239
240pub fn tcv1_sha256(value: &TypedCanonicalValue) -> Result<String, Tcv1Error> {
242 let digest = Sha256::digest(tcv1_preimage(value)?);
243 let mut output = String::with_capacity(digest.len() * 2);
244 for byte in digest {
245 write!(&mut output, "{byte:02x}").expect("writing to String cannot fail");
246 }
247 Ok(output)
248}
249
250fn normalize(value: &str) -> String {
251 value.nfc().collect()
252}
253
254fn encode_value(
255 value: &TypedCanonicalValue,
256 output: &mut Vec<u8>,
257 depth: usize,
258) -> Result<(), Tcv1Error> {
259 if depth > MAX_NESTING_DEPTH {
260 return Err(Tcv1Error::NestingTooDeep);
261 }
262 match value {
263 TypedCanonicalValue::Null => output.push(b'N'),
264 TypedCanonicalValue::Bool(false) => output.push(b'F'),
265 TypedCanonicalValue::Bool(true) => output.push(b'T'),
266 TypedCanonicalValue::Integer(value) => {
267 output.push(b'I');
268 let payload = value.decimal();
269 encode_length(payload.len(), output)?;
270 output.extend_from_slice(payload.as_bytes());
271 }
272 TypedCanonicalValue::Binary64(value) => {
273 if !value.is_finite() {
274 return Err(Tcv1Error::NonFiniteBinary64);
275 }
276 output.push(b'D');
277 let normalized = if *value == 0.0 { 0.0 } else { *value };
278 output.extend_from_slice(&normalized.to_bits().to_be_bytes());
279 }
280 TypedCanonicalValue::String(value) => encode_string(value, output)?,
281 TypedCanonicalValue::Array(values) => {
282 output.push(b'A');
283 encode_length(values.len(), output)?;
284 for value in values {
285 encode_value(value, output, depth + 1)?;
286 }
287 }
288 TypedCanonicalValue::Object(entries) => {
289 output.push(b'O');
290 encode_length(entries.len(), output)?;
291
292 let mut raw_keys = HashSet::with_capacity(entries.len());
293 let mut normalized_keys = HashMap::with_capacity(entries.len());
294 let mut sorted = Vec::with_capacity(entries.len());
295 for (key, value) in entries {
296 if !raw_keys.insert(key.as_str()) {
297 return Err(Tcv1Error::DuplicateObjectKey {
298 key: key.clone(),
299 offset: 0,
300 });
301 }
302 let normalized = normalize(key);
303 if let Some(first) = normalized_keys.insert(normalized.clone(), key.as_str()) {
304 return Err(Tcv1Error::NfcKeyCollision {
305 first: first.to_string(),
306 second: key.clone(),
307 offset: 0,
308 });
309 }
310 sorted.push((normalized.into_bytes(), value));
311 }
312 sorted.sort_by(|left, right| left.0.cmp(&right.0));
313 for (normalized_key, value) in sorted {
314 encode_normalized_string(&normalized_key, output)?;
315 encode_value(value, output, depth + 1)?;
316 }
317 }
318 }
319 Ok(())
320}
321
322fn encode_string(value: &str, output: &mut Vec<u8>) -> Result<(), Tcv1Error> {
323 let normalized = normalize(value);
324 encode_normalized_string(normalized.as_bytes(), output)
325}
326
327fn encode_normalized_string(payload: &[u8], output: &mut Vec<u8>) -> Result<(), Tcv1Error> {
328 output.push(b'S');
329 encode_length(payload.len(), output)?;
330 output.extend_from_slice(payload);
331 Ok(())
332}
333
334fn encode_length(length: usize, output: &mut Vec<u8>) -> Result<(), Tcv1Error> {
335 let length = u64::try_from(length).map_err(|_| Tcv1Error::LengthOverflow)?;
336 output.extend_from_slice(&length.to_be_bytes());
337 Ok(())
338}
339
340struct Parser<'a> {
341 input: &'a str,
342 bytes: &'a [u8],
343 offset: usize,
344}
345
346impl<'a> Parser<'a> {
347 fn new(input: &'a str) -> Self {
348 Self {
349 input,
350 bytes: input.as_bytes(),
351 offset: 0,
352 }
353 }
354
355 fn parse_value(&mut self, depth: usize) -> Result<TypedCanonicalValue, Tcv1Error> {
356 if depth > MAX_NESTING_DEPTH {
357 return Err(Tcv1Error::NestingTooDeep);
358 }
359 self.skip_whitespace();
360 match self.peek() {
361 Some(b'n') => {
362 self.consume_literal(b"null")?;
363 Ok(TypedCanonicalValue::Null)
364 }
365 Some(b'f') => {
366 self.consume_literal(b"false")?;
367 Ok(TypedCanonicalValue::Bool(false))
368 }
369 Some(b't') => {
370 self.consume_literal(b"true")?;
371 Ok(TypedCanonicalValue::Bool(true))
372 }
373 Some(b'"') => Ok(TypedCanonicalValue::String(self.parse_string()?)),
374 Some(b'[') => self.parse_array(depth),
375 Some(b'{') => self.parse_object(depth),
376 Some(b'-' | b'0'..=b'9') => self.parse_number(),
377 Some(_) => Err(self.invalid("expected a JSON value")),
378 None => Err(self.invalid("unexpected end of input")),
379 }
380 }
381
382 fn parse_array(&mut self, depth: usize) -> Result<TypedCanonicalValue, Tcv1Error> {
383 self.offset += 1;
384 self.skip_whitespace();
385 let mut values = Vec::new();
386 if self.consume_if(b']') {
387 return Ok(TypedCanonicalValue::Array(values));
388 }
389 loop {
390 values.push(self.parse_value(depth + 1)?);
391 self.skip_whitespace();
392 match self.peek() {
393 Some(b',') => self.offset += 1,
394 Some(b']') => {
395 self.offset += 1;
396 return Ok(TypedCanonicalValue::Array(values));
397 }
398 Some(_) => return Err(self.invalid("expected `,` or `]` in array")),
399 None => return Err(self.invalid("unterminated array")),
400 }
401 }
402 }
403
404 fn parse_object(&mut self, depth: usize) -> Result<TypedCanonicalValue, Tcv1Error> {
405 self.offset += 1;
406 self.skip_whitespace();
407 let mut entries = Vec::new();
408 let mut raw_keys = HashSet::new();
409 let mut normalized_keys: HashMap<String, String> = HashMap::new();
410 if self.consume_if(b'}') {
411 return Ok(TypedCanonicalValue::Object(entries));
412 }
413 loop {
414 self.skip_whitespace();
415 if self.peek() != Some(b'"') {
416 return Err(self.invalid("expected a string object key"));
417 }
418 let key_offset = self.offset;
419 let key = self.parse_string()?;
420 if !raw_keys.insert(key.clone()) {
421 return Err(Tcv1Error::DuplicateObjectKey {
422 key,
423 offset: key_offset,
424 });
425 }
426 let normalized = normalize(&key);
427 if let Some(first) = normalized_keys.insert(normalized, key.clone()) {
428 return Err(Tcv1Error::NfcKeyCollision {
429 first,
430 second: key,
431 offset: key_offset,
432 });
433 }
434 self.skip_whitespace();
435 self.expect(b':', "expected `:` after object key")?;
436 let value = self.parse_value(depth + 1)?;
437 entries.push((key, value));
438 self.skip_whitespace();
439 match self.peek() {
440 Some(b',') => self.offset += 1,
441 Some(b'}') => {
442 self.offset += 1;
443 return Ok(TypedCanonicalValue::Object(entries));
444 }
445 Some(_) => return Err(self.invalid("expected `,` or `}` in object")),
446 None => return Err(self.invalid("unterminated object")),
447 }
448 }
449 }
450
451 fn parse_number(&mut self) -> Result<TypedCanonicalValue, Tcv1Error> {
452 let start = self.offset;
453 let negative = self.consume_if(b'-');
454
455 match self.peek() {
456 Some(b'0') => {
457 self.offset += 1;
458 if matches!(self.peek(), Some(b'0'..=b'9')) {
459 return Err(self.invalid("leading zero in JSON number"));
460 }
461 }
462 Some(b'1'..=b'9') => {
463 self.offset += 1;
464 while matches!(self.peek(), Some(b'0'..=b'9')) {
465 self.offset += 1;
466 }
467 }
468 _ => return Err(self.invalid("expected integer digits")),
469 }
470
471 let mut binary64 = false;
472 if self.consume_if(b'.') {
473 binary64 = true;
474 if !matches!(self.peek(), Some(b'0'..=b'9')) {
475 return Err(self.invalid("expected digit after decimal point"));
476 }
477 while matches!(self.peek(), Some(b'0'..=b'9')) {
478 self.offset += 1;
479 }
480 }
481 if matches!(self.peek(), Some(b'e' | b'E')) {
482 binary64 = true;
483 self.offset += 1;
484 if matches!(self.peek(), Some(b'+' | b'-')) {
485 self.offset += 1;
486 }
487 if !matches!(self.peek(), Some(b'0'..=b'9')) {
488 return Err(self.invalid("expected exponent digits"));
489 }
490 while matches!(self.peek(), Some(b'0'..=b'9')) {
491 self.offset += 1;
492 }
493 }
494
495 let token = &self.input[start..self.offset];
496 if binary64 {
497 let value = token
498 .parse::<f64>()
499 .map_err(|_| Tcv1Error::Binary64OutOfRange { offset: start })?;
500 if !value.is_finite() {
501 return Err(Tcv1Error::Binary64OutOfRange { offset: start });
502 }
503 Ok(TypedCanonicalValue::Binary64(value))
504 } else if negative {
505 token
506 .parse::<i64>()
507 .map(|value| TypedCanonicalValue::Integer(CanonicalInteger::Signed(value)))
508 .map_err(|_| Tcv1Error::IntegerOutOfRange {
509 offset: start,
510 domain: "i64",
511 })
512 } else {
513 token
514 .parse::<u64>()
515 .map(|value| TypedCanonicalValue::Integer(CanonicalInteger::Unsigned(value)))
516 .map_err(|_| Tcv1Error::IntegerOutOfRange {
517 offset: start,
518 domain: "u64",
519 })
520 }
521 }
522
523 fn parse_string(&mut self) -> Result<String, Tcv1Error> {
524 debug_assert_eq!(self.peek(), Some(b'"'));
525 self.offset += 1;
526 let mut output = String::new();
527 let mut chunk_start = self.offset;
528 loop {
529 match self.peek() {
530 Some(b'"') => {
531 output.push_str(&self.input[chunk_start..self.offset]);
532 self.offset += 1;
533 return Ok(output);
534 }
535 Some(b'\\') => {
536 output.push_str(&self.input[chunk_start..self.offset]);
537 self.offset += 1;
538 self.parse_escape(&mut output)?;
539 chunk_start = self.offset;
540 }
541 Some(0x00..=0x1f) => {
542 return Err(self.invalid("unescaped control character in JSON string"));
543 }
544 Some(byte) if byte.is_ascii() => self.offset += 1,
545 Some(_) => {
546 let character = self.input[self.offset..]
547 .chars()
548 .next()
549 .expect("offset is on a valid UTF-8 boundary");
550 self.offset += character.len_utf8();
551 }
552 None => return Err(self.invalid("unterminated JSON string")),
553 }
554 }
555 }
556
557 fn parse_escape(&mut self, output: &mut String) -> Result<(), Tcv1Error> {
558 match self.peek() {
559 Some(b'"') => output.push('"'),
560 Some(b'\\') => output.push('\\'),
561 Some(b'/') => output.push('/'),
562 Some(b'b') => output.push('\u{0008}'),
563 Some(b'f') => output.push('\u{000c}'),
564 Some(b'n') => output.push('\n'),
565 Some(b'r') => output.push('\r'),
566 Some(b't') => output.push('\t'),
567 Some(b'u') => {
568 self.offset += 1;
569 return self.parse_unicode_escape(output);
570 }
571 Some(_) => return Err(self.invalid("invalid JSON string escape")),
572 None => return Err(self.invalid("unterminated JSON string escape")),
573 }
574 self.offset += 1;
575 Ok(())
576 }
577
578 fn parse_unicode_escape(&mut self, output: &mut String) -> Result<(), Tcv1Error> {
579 let first_offset = self.offset;
580 let first = self.parse_hex_quad()?;
581 let scalar = if (0xd800..=0xdbff).contains(&first) {
582 if self.peek() != Some(b'\\') || self.bytes.get(self.offset + 1) != Some(&b'u') {
583 return Err(Tcv1Error::InvalidJson {
584 offset: first_offset,
585 message: "high surrogate is not followed by a low surrogate",
586 });
587 }
588 self.offset += 2;
589 let low_offset = self.offset;
590 let low = self.parse_hex_quad()?;
591 if !(0xdc00..=0xdfff).contains(&low) {
592 return Err(Tcv1Error::InvalidJson {
593 offset: low_offset,
594 message: "high surrogate is not followed by a low surrogate",
595 });
596 }
597 0x10000 + ((u32::from(first) - 0xd800) << 10) + (u32::from(low) - 0xdc00)
598 } else if (0xdc00..=0xdfff).contains(&first) {
599 return Err(Tcv1Error::InvalidJson {
600 offset: first_offset,
601 message: "isolated low surrogate in JSON string",
602 });
603 } else {
604 u32::from(first)
605 };
606 output.push(char::from_u32(scalar).expect("validated Unicode scalar value"));
607 Ok(())
608 }
609
610 fn parse_hex_quad(&mut self) -> Result<u16, Tcv1Error> {
611 if self.offset + 4 > self.bytes.len() {
612 return Err(self.invalid("incomplete Unicode escape"));
613 }
614 let mut value = 0_u16;
615 for _ in 0..4 {
616 let digit = match self.peek() {
617 Some(b'0'..=b'9') => u16::from(self.bytes[self.offset] - b'0'),
618 Some(b'a'..=b'f') => u16::from(self.bytes[self.offset] - b'a' + 10),
619 Some(b'A'..=b'F') => u16::from(self.bytes[self.offset] - b'A' + 10),
620 _ => return Err(self.invalid("invalid hexadecimal digit in Unicode escape")),
621 };
622 value = (value << 4) | digit;
623 self.offset += 1;
624 }
625 Ok(value)
626 }
627
628 fn consume_literal(&mut self, literal: &[u8]) -> Result<(), Tcv1Error> {
629 if self.bytes.get(self.offset..self.offset + literal.len()) == Some(literal) {
630 self.offset += literal.len();
631 Ok(())
632 } else {
633 Err(self.invalid("invalid JSON literal"))
634 }
635 }
636
637 fn expect(&mut self, expected: u8, message: &'static str) -> Result<(), Tcv1Error> {
638 if self.consume_if(expected) {
639 Ok(())
640 } else {
641 Err(self.invalid(message))
642 }
643 }
644
645 fn consume_if(&mut self, expected: u8) -> bool {
646 if self.peek() == Some(expected) {
647 self.offset += 1;
648 true
649 } else {
650 false
651 }
652 }
653
654 fn skip_whitespace(&mut self) {
655 while matches!(self.peek(), Some(b' ' | b'\n' | b'\r' | b'\t')) {
656 self.offset += 1;
657 }
658 }
659
660 fn peek(&self) -> Option<u8> {
661 self.bytes.get(self.offset).copied()
662 }
663
664 fn invalid(&self, message: &'static str) -> Tcv1Error {
665 Tcv1Error::InvalidJson {
666 offset: self.offset,
667 message,
668 }
669 }
670}
671
672fn validate_external_container_shapes(
677 raw: &serde_json::Value,
678 typed: &serde_json::Value,
679 path: &str,
680) -> std::result::Result<(), String> {
681 match (raw, typed) {
682 (serde_json::Value::Object(raw), serde_json::Value::Object(typed)) => {
683 for (key, raw_value) in raw {
684 if let Some(typed_value) = typed.get(key) {
685 validate_external_container_shapes(
686 raw_value,
687 typed_value,
688 &format!("{path}.{key}"),
689 )?;
690 }
691 }
692 Ok(())
693 }
694 (serde_json::Value::Array(raw), serde_json::Value::Array(typed)) => {
695 for (index, (raw_value, typed_value)) in raw.iter().zip(typed).enumerate() {
696 validate_external_container_shapes(
697 raw_value,
698 typed_value,
699 &format!("{path}[{index}]"),
700 )?;
701 }
702 Ok(())
703 }
704 (_, serde_json::Value::Object(_)) => Err(format!(
705 "{path} must use a JSON object at the external contract boundary"
706 )),
707 (_, serde_json::Value::Array(_)) => Err(format!(
708 "{path} must use a JSON array at the external contract boundary"
709 )),
710 (serde_json::Value::Object(_) | serde_json::Value::Array(_), _) => Err(format!(
711 "{path} has the wrong JSON container kind at the external contract boundary"
712 )),
713 _ => Ok(()),
714 }
715}
716
717pub fn deserialize_external_contract<T, F>(
721 json: &str,
722 label: &str,
723 shape_error: F,
724) -> DagMlResult<T>
725where
726 T: DeserializeOwned + Serialize,
727 F: Fn(String) -> DagMlError,
728{
729 parse_typed_json(json).map_err(|error| {
730 shape_error(format!(
731 "{label} is not a strict TCV1 JSON document: {error}"
732 ))
733 })?;
734 let raw: serde_json::Value = serde_json::from_str(json)?;
735 deserialize_external_value(raw, label, shape_error)
736}
737
738pub fn deserialize_external_value<T, F>(
742 raw: serde_json::Value,
743 label: &str,
744 shape_error: F,
745) -> DagMlResult<T>
746where
747 T: DeserializeOwned + Serialize,
748 F: Fn(String) -> DagMlError,
749{
750 validate_typed_serde_value(&raw).map_err(|error| {
751 shape_error(format!(
752 "{label} is not a strict TCV1 structured value: {error}"
753 ))
754 })?;
755 let value: T = serde_json::from_value(raw.clone())?;
756 let typed = serde_json::to_value(&value)?;
757 validate_external_container_shapes(&raw, &typed, label).map_err(shape_error)?;
758 Ok(value)
759}
760
761#[cfg(test)]
762mod tests {
763 use serde::{Deserialize, Serialize};
764 use serde_json::json;
765
766 use super::*;
767
768 fn parse(input: &str) -> TypedCanonicalValue {
769 parse_typed_json(input).expect("valid strict JSON")
770 }
771
772 fn hex(bytes: &[u8]) -> String {
773 let mut output = String::with_capacity(bytes.len() * 2);
774 for byte in bytes {
775 write!(&mut output, "{byte:02x}").unwrap();
776 }
777 output
778 }
779
780 fn assert_vector(input: &str, expected_preimage: &str, expected_sha256: &str) {
781 let value = parse(input);
782 assert_eq!(hex(&tcv1_preimage(&value).unwrap()), expected_preimage);
783 assert_eq!(tcv1_sha256(&value).unwrap(), expected_sha256);
784 }
785
786 #[derive(Debug, Deserialize)]
787 struct GoldenFixture {
788 tcv1_vectors: Vec<GoldenVector>,
789 }
790
791 #[derive(Debug, Deserialize)]
792 struct GoldenVector {
793 id: String,
794 document_json: String,
795 equivalent_json: Option<String>,
796 expected_preimage_hex: String,
797 expected_sha256: String,
798 }
799
800 #[derive(Debug, Deserialize, PartialEq, Serialize)]
801 struct ExternalFixture {
802 schema_version: u64,
803 values: Vec<u64>,
804 }
805
806 #[test]
807 fn production_tcv1_matches_every_committed_golden_vector() {
808 let fixture: GoldenFixture = serde_json::from_str(include_str!(
809 "../../../parity/canonical/golden/tcv1_jcs_cross_language.v1.json"
810 ))
811 .expect("valid committed TCV1 golden fixture");
812
813 for vector in fixture.tcv1_vectors {
814 assert_vector(
815 &vector.document_json,
816 &vector.expected_preimage_hex,
817 &vector.expected_sha256,
818 );
819 if let Some(equivalent) = vector.equivalent_json {
820 assert_eq!(
821 tcv1_preimage(&parse(&vector.document_json)).unwrap(),
822 tcv1_preimage(&parse(&equivalent)).unwrap(),
823 "{}",
824 vector.id
825 );
826 }
827 }
828 }
829
830 #[test]
831 fn external_contract_boundary_is_strict_and_shape_preserving() {
832 let value: ExternalFixture = deserialize_external_contract(
833 r#"{"schema_version":1,"values":[2,3]}"#,
834 "fixture",
835 DagMlError::RuntimeValidation,
836 )
837 .expect("object-form contract is valid");
838 assert_eq!(
839 value,
840 ExternalFixture {
841 schema_version: 1,
842 values: vec![2, 3],
843 }
844 );
845
846 let duplicate = deserialize_external_contract::<ExternalFixture, _>(
847 r#"{"schema_version":1,"schema_version":2,"values":[]}"#,
848 "fixture",
849 DagMlError::RuntimeValidation,
850 )
851 .unwrap_err();
852 assert!(duplicate.to_string().contains("duplicate JSON object key"));
853
854 let positional = deserialize_external_contract::<ExternalFixture, _>(
855 r#"[1,[]]"#,
856 "fixture",
857 DagMlError::RuntimeValidation,
858 )
859 .unwrap_err();
860 assert!(positional.to_string().contains("must use a JSON object"));
861 }
862
863 #[test]
864 fn golden_map_order_preimage_and_digest() {
865 const PREIMAGE: &str = "4441474d4c2d54435631004f000000000000000253000000000000000161490000000000000001325300000000000000017a49000000000000000131";
866 const DIGEST: &str = "5441a8df23725b4a60e16316f3034a7ec8b25b853ce75600fa71dda19c8a16e1";
867 assert_vector(r#"{"z":1,"a":2}"#, PREIMAGE, DIGEST);
868 assert_vector(r#"{"a":2,"z":1}"#, PREIMAGE, DIGEST);
869 }
870
871 #[test]
872 fn object_order_uses_normalized_utf8_not_utf16() {
873 assert_vector(
874 r#"{"\ue000":1,"\ud800\udc00":2}"#,
875 "4441474d4c2d54435631004f0000000000000002530000000000000003ee808049000000000000000131530000000000000004f090808049000000000000000132",
876 "7c212789a6d362b8a34e8c271d5fe003c2026a828712eef6469610eedda79bc7",
877 );
878 }
879
880 #[test]
881 fn nfc_normalizes_strings_and_keys() {
882 let decomposed = parse(r#""e\u0301""#);
883 let composed = parse(r#""é""#);
884 assert_ne!(decomposed, composed);
887 assert_eq!(
888 tcv1_preimage(&decomposed).unwrap(),
889 tcv1_preimage(&composed).unwrap()
890 );
891 assert_eq!(
892 tcv1_sha256(&decomposed).unwrap(),
893 "a4af538cebb2c18fed88a1ad4245509500d201e68802a217e4e8500ef61c0e86"
894 );
895
896 let error = parse_typed_json(r#"{"é":1,"e\u0301":2}"#).unwrap_err();
897 assert!(matches!(error, Tcv1Error::NfcKeyCollision { .. }));
898 }
899
900 #[test]
901 fn signed_zero_normalizes_only_within_binary64_kind() {
902 let negative = parse("-0.0");
903 let positive = parse("0.0");
904 assert_eq!(
905 tcv1_preimage(&negative).unwrap(),
906 tcv1_preimage(&positive).unwrap()
907 );
908 assert_vector(
909 "-0.0",
910 "4441474d4c2d5443563100440000000000000000",
911 "c01f83d2f6a8e96eb7f50c4794eef0dbae68ad4d20ed116af013ae5cd4ffa49d",
912 );
913 assert_ne!(
914 tcv1_preimage(&parse("-0")).unwrap(),
915 tcv1_preimage(&negative).unwrap()
916 );
917 assert_eq!(
918 tcv1_preimage(&parse("-0")).unwrap(),
919 tcv1_preimage(&parse("0")).unwrap()
920 );
921 }
922
923 #[test]
924 fn integer_and_binary64_tokens_remain_distinct() {
925 assert_eq!(
926 parse("2"),
927 TypedCanonicalValue::Integer(CanonicalInteger::Unsigned(2))
928 );
929 assert_eq!(parse("2.0"), TypedCanonicalValue::Binary64(2.0));
930 assert_ne!(
931 tcv1_preimage(&parse("2")).unwrap(),
932 tcv1_preimage(&parse("2.0")).unwrap()
933 );
934 assert_vector(
935 "2",
936 "4441474d4c2d544356310049000000000000000132",
937 "3940883272509c80c7bbff602794dce0f62dfa7850bc3041b37c56d36bc94701",
938 );
939 let float_preimage = tcv1_preimage(&parse("2.0")).unwrap();
940 assert_eq!(float_preimage, tcv1_preimage(&parse("2e0")).unwrap());
941 assert_eq!(
942 hex(&float_preimage),
943 "4441474d4c2d5443563100444000000000000000"
944 );
945 }
946
947 #[test]
948 fn frozen_binary64_boundary_vectors() {
949 let vectors = [
950 (
951 "5e-324",
952 "4441474d4c2d5443563100440000000000000001",
953 "88a7b6becacc6cf0bf2473332aa17d9f3ed513b4024d69684458a275b5c39c24",
954 ),
955 (
956 "2.2250738585072009e-308",
957 "4441474d4c2d544356310044000fffffffffffff",
958 "78c8b93679333797971ff7ef4dba4b284adb03da7a7379bf814f635b69164765",
959 ),
960 (
961 "2.2250738585072014e-308",
962 "4441474d4c2d5443563100440010000000000000",
963 "6ea1aeba7ec435fd15165511f602295eedd7de82832713f35891600a0f552702",
964 ),
965 (
966 "9007199254740992.0",
967 "4441474d4c2d5443563100444340000000000000",
968 "8e276db087fa6f18be879c6e32a034e44ff075c66c76ff8e43cbd3dc20e0673a",
969 ),
970 (
971 "1.7976931348623157e308",
972 "4441474d4c2d5443563100447fefffffffffffff",
973 "e9231aadbc74db0fd07f62e1b04c67ab93a73c30b756e85e36f80edd3766bf5a",
974 ),
975 ];
976 for (input, preimage, digest) in vectors {
977 assert_vector(input, preimage, digest);
978 }
979 assert_eq!(
980 tcv1_preimage(&parse("5e-324")).unwrap(),
981 tcv1_preimage(&parse("4.9406564584124654e-324")).unwrap()
982 );
983 assert_eq!(
984 tcv1_preimage(&parse("9007199254740992.0")).unwrap(),
985 tcv1_preimage(&parse("9.007199254740992e15")).unwrap()
986 );
987 }
988
989 #[test]
990 fn all_tags_and_big_endian_lengths_are_explicit() {
991 let value = parse(r#"[null,false,true,"x",-1,1.5]"#);
992 assert_eq!(
993 hex(&tcv1_encode(&value).unwrap()),
994 "4100000000000000064e4654530000000000000001784900000000000000022d31443ff8000000000000"
995 );
996 assert_vector(
997 "[]",
998 "4441474d4c2d5443563100410000000000000000",
999 "cea5f239e81001721b763cebf40cd71bca04972c51313fba335e0a96d7e81979",
1000 );
1001 assert_vector(
1002 "{}",
1003 "4441474d4c2d54435631004f0000000000000000",
1004 "05fb75f2c266555e97a65becbafc84f8dc52b9f4cb2da8f7b7c5bfc8073325f2",
1005 );
1006 }
1007
1008 #[test]
1009 fn integer_bounds_are_lexical_and_exact() {
1010 assert_eq!(
1011 parse("-9223372036854775808"),
1012 TypedCanonicalValue::Integer(CanonicalInteger::Signed(i64::MIN))
1013 );
1014 assert_eq!(
1015 parse("18446744073709551615"),
1016 TypedCanonicalValue::Integer(CanonicalInteger::Unsigned(u64::MAX))
1017 );
1018 assert!(matches!(
1019 parse_typed_json("-9223372036854775809"),
1020 Err(Tcv1Error::IntegerOutOfRange { domain: "i64", .. })
1021 ));
1022 assert!(matches!(
1023 parse_typed_json("18446744073709551616"),
1024 Err(Tcv1Error::IntegerOutOfRange { domain: "u64", .. })
1025 ));
1026 }
1027
1028 #[test]
1029 fn strict_parser_rejects_invalid_documents() {
1030 assert!(matches!(
1031 parse_typed_json(r#"{"a":1,"a":2}"#),
1032 Err(Tcv1Error::DuplicateObjectKey { .. })
1033 ));
1034 assert!(matches!(
1035 parse_typed_json(r#"{"a":1,"\u0061":2}"#),
1036 Err(Tcv1Error::DuplicateObjectKey { .. })
1037 ));
1038 for document in [r#""\ud800""#, r#""\udc00""#, r#""\ud800\u0061""#] {
1039 assert!(matches!(
1040 parse_typed_json(document),
1041 Err(Tcv1Error::InvalidJson { .. })
1042 ));
1043 }
1044 assert!(matches!(
1045 parse_typed_json("1e400"),
1046 Err(Tcv1Error::Binary64OutOfRange { .. })
1047 ));
1048 assert!(matches!(
1049 parse_typed_json_bytes(&[b'"', 0xff, b'"']),
1050 Err(Tcv1Error::InvalidUtf8 { .. })
1051 ));
1052 for document in ["null true", "01", "1.", "1e", "[1,]", r#"{"a":1,}"#] {
1053 assert!(parse_typed_json(document).is_err(), "accepted {document:?}");
1054 }
1055 }
1056
1057 #[test]
1058 fn programmatic_values_receive_the_same_safety_checks() {
1059 assert_eq!(
1060 tcv1_encode(&TypedCanonicalValue::Binary64(f64::INFINITY)),
1061 Err(Tcv1Error::NonFiniteBinary64)
1062 );
1063 assert_eq!(
1064 tcv1_encode(&TypedCanonicalValue::Binary64(f64::NAN)),
1065 Err(Tcv1Error::NonFiniteBinary64)
1066 );
1067 let collision = TypedCanonicalValue::Object(vec![
1068 ("é".to_string(), TypedCanonicalValue::Null),
1069 ("e\u{301}".to_string(), TypedCanonicalValue::Null),
1070 ]);
1071 assert!(matches!(
1072 tcv1_encode(&collision),
1073 Err(Tcv1Error::NfcKeyCollision { .. })
1074 ));
1075 let mut structured_collision = serde_json::Map::new();
1076 structured_collision.insert("é".to_string(), serde_json::Value::Null);
1077 structured_collision.insert("e\u{301}".to_string(), serde_json::Value::Null);
1078 assert!(matches!(
1079 validate_typed_serde_value(&serde_json::Value::Object(structured_collision)),
1080 Err(Tcv1Error::NfcKeyCollision { .. })
1081 ));
1082 }
1083
1084 #[test]
1085 fn self_fingerprint_omits_exactly_one_normalized_key() {
1086 let with_fingerprint = parse(r#"{"payload":2,"fingerprint":"pending"}"#);
1087 let payload_only = parse(r#"{"payload":2}"#);
1088 assert_eq!(
1089 with_fingerprint.fingerprint_without("fingerprint").unwrap(),
1090 payload_only.fingerprint().unwrap()
1091 );
1092 assert!(matches!(
1093 payload_only.fingerprint_without("fingerprint"),
1094 Err(Tcv1Error::MissingObjectKey(_))
1095 ));
1096 assert!(matches!(
1097 parse("[]").fingerprint_without("fingerprint"),
1098 Err(Tcv1Error::ExpectedObject)
1099 ));
1100
1101 let decomposed_key = parse(r#"{"empreinte\u0301":"pending","payload":2}"#);
1102 assert_eq!(
1103 decomposed_key.fingerprint_without("empreinté").unwrap(),
1104 payload_only.fingerprint().unwrap()
1105 );
1106 let ambiguous = TypedCanonicalValue::Object(vec![
1107 ("é".to_string(), TypedCanonicalValue::Null),
1108 ("e\u{301}".to_string(), TypedCanonicalValue::Null),
1109 ]);
1110 assert!(matches!(
1111 ambiguous.fingerprint_without("é"),
1112 Err(Tcv1Error::AmbiguousObjectKey(_))
1113 ));
1114 }
1115
1116 #[test]
1117 fn parser_and_encoder_enforce_the_nesting_limit() {
1118 let accepted = format!(
1119 "{}0{}",
1120 "[".repeat(MAX_NESTING_DEPTH),
1121 "]".repeat(MAX_NESTING_DEPTH)
1122 );
1123 let accepted = parse_typed_json(&accepted).expect("boundary depth is accepted");
1124 tcv1_encode(&accepted).expect("encoder accepts the same boundary depth");
1125
1126 let rejected = format!(
1127 "{}0{}",
1128 "[".repeat(MAX_NESTING_DEPTH + 1),
1129 "]".repeat(MAX_NESTING_DEPTH + 1)
1130 );
1131 assert_eq!(parse_typed_json(&rejected), Err(Tcv1Error::NestingTooDeep));
1132
1133 let mut programmatic = TypedCanonicalValue::Null;
1134 for _ in 0..=MAX_NESTING_DEPTH {
1135 programmatic = TypedCanonicalValue::Array(vec![programmatic]);
1136 }
1137 assert_eq!(tcv1_encode(&programmatic), Err(Tcv1Error::NestingTooDeep));
1138
1139 let accepted_objects = format!(
1140 "{}0{}",
1141 r#"{"a":"#.repeat(MAX_NESTING_DEPTH),
1142 "}".repeat(MAX_NESTING_DEPTH)
1143 );
1144 let accepted_objects =
1145 parse_typed_json(&accepted_objects).expect("object boundary depth is accepted");
1146 tcv1_encode(&accepted_objects).expect("encoder accepts object boundary depth");
1147
1148 let rejected_objects = format!(
1149 "{}0{}",
1150 r#"{"a":"#.repeat(MAX_NESTING_DEPTH + 1),
1151 "}".repeat(MAX_NESTING_DEPTH + 1)
1152 );
1153 assert_eq!(
1154 parse_typed_json(&rejected_objects),
1155 Err(Tcv1Error::NestingTooDeep)
1156 );
1157 }
1158
1159 #[test]
1160 fn historical_stable_json_fingerprint_does_not_drift() {
1161 let value = json!({"a": 2, "z": [true, null]});
1162 assert_eq!(
1163 crate::campaign::stable_json_fingerprint(&value).unwrap(),
1164 "b4f8d6fce8a1198ebca7d0206f8c229dfe7a0c663929b0df2d72053d3d34624a"
1165 );
1166 }
1167}