continuity_receipt/
canon.rs1use std::fmt;
9
10use serde_json::Value;
11use sha2::{Digest, Sha256};
12
13const HEX: &[u8; 16] = b"0123456789abcdef";
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct CanonError(pub String);
18
19impl fmt::Display for CanonError {
20 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
21 formatter.write_str(&self.0)
22 }
23}
24
25impl std::error::Error for CanonError {}
26
27pub fn canonical_bytes(value: &Value) -> Result<Vec<u8>, CanonError> {
29 let mut out = Vec::new();
30 write_value(value, &mut out)?;
31 Ok(out)
32}
33
34fn write_value(value: &Value, out: &mut Vec<u8>) -> Result<(), CanonError> {
35 match value {
36 Value::Null => out.extend_from_slice(b"null"),
37 Value::Bool(true) => out.extend_from_slice(b"true"),
38 Value::Bool(false) => out.extend_from_slice(b"false"),
39 Value::Number(number) => {
40 let token = number.to_string();
41 if token.contains('.') || token.contains('e') || token.contains('E') {
42 return Err(CanonError(
43 "floats are not allowed in continuity receipts (v0)".to_string(),
44 ));
45 }
46 out.extend_from_slice(token.as_bytes());
47 }
48 Value::String(text) => write_string(text, out),
49 Value::Array(items) => {
50 out.push(b'[');
51 for (index, item) in items.iter().enumerate() {
52 if index > 0 {
53 out.push(b',');
54 }
55 write_value(item, out)?;
56 }
57 out.push(b']');
58 }
59 Value::Object(map) => {
60 let mut keys: Vec<&String> = map.keys().collect();
61 keys.sort_unstable();
62 out.push(b'{');
63 for (index, key) in keys.iter().enumerate() {
64 if index > 0 {
65 out.push(b',');
66 }
67 write_string(key, out);
68 out.push(b':');
69 if let Some(item) = map.get(key.as_str()) {
70 write_value(item, out)?;
71 }
72 }
73 out.push(b'}');
74 }
75 }
76 Ok(())
77}
78
79fn write_string(text: &str, out: &mut Vec<u8>) {
81 out.push(b'"');
82 for character in text.chars() {
83 match character {
84 '"' => out.extend_from_slice(b"\\\""),
85 '\\' => out.extend_from_slice(b"\\\\"),
86 '\u{08}' => out.extend_from_slice(b"\\b"),
87 '\u{0c}' => out.extend_from_slice(b"\\f"),
88 '\n' => out.extend_from_slice(b"\\n"),
89 '\r' => out.extend_from_slice(b"\\r"),
90 '\t' => out.extend_from_slice(b"\\t"),
91 c if (c as u32) < 0x20 => {
92 let escape = format!("\\u{:04x}", c as u32);
93 out.extend_from_slice(escape.as_bytes());
94 }
95 c => {
96 let mut buffer = [0u8; 4];
97 out.extend_from_slice(c.encode_utf8(&mut buffer).as_bytes());
98 }
99 }
100 }
101 out.push(b'"');
102}
103
104pub fn sha256_prefixed(data: &[u8]) -> String {
106 let digest = Sha256::digest(data);
107 let mut out = String::with_capacity(7 + 64);
108 out.push_str("sha256:");
109 for byte in digest {
110 out.push(HEX[(byte >> 4) as usize] as char);
111 out.push(HEX[(byte & 0x0f) as usize] as char);
112 }
113 out
114}
115
116pub fn commit_field(salt_hex: &str, value: &Value) -> Result<String, CanonError> {
120 let salt = hex_decode(salt_hex)
121 .ok_or_else(|| CanonError(format!("invalid salt hex: {salt_hex:?}")))?;
122 let mut data = salt;
123 data.push(0x7c);
124 data.extend_from_slice(&canonical_bytes(value)?);
125 Ok(sha256_prefixed(&data))
126}
127
128fn hex_decode(text: &str) -> Option<Vec<u8>> {
129 let bytes = text.as_bytes();
130 if !bytes.len().is_multiple_of(2) {
131 return None;
132 }
133 let mut out = Vec::with_capacity(bytes.len() / 2);
134 let mut index = 0;
135 while index < bytes.len() {
136 let high = hex_value(bytes[index])?;
137 let low = hex_value(bytes[index + 1])?;
138 out.push((high << 4) | low);
139 index += 2;
140 }
141 Some(out)
142}
143
144fn hex_value(byte: u8) -> Option<u8> {
145 match byte {
146 b'0'..=b'9' => Some(byte - b'0'),
147 b'a'..=b'f' => Some(byte - b'a' + 10),
148 b'A'..=b'F' => Some(byte - b'A' + 10),
149 _ => None,
150 }
151}