1use sha2::{Digest, Sha256};
12use std::collections::{BTreeMap, BTreeSet};
13
14trait Sink {
17 fn put(&mut self, bytes: &[u8]);
18}
19
20impl Sink for Sha256 {
21 #[inline]
22 fn put(&mut self, bytes: &[u8]) {
23 self.update(bytes);
24 }
25}
26
27impl Sink for Vec<u8> {
28 #[inline]
29 fn put(&mut self, bytes: &[u8]) {
30 self.extend_from_slice(bytes);
31 }
32}
33
34const HEX: &[u8; 16] = b"0123456789abcdef";
35
36fn put_json_string<S: Sink>(sink: &mut S, value: &str) {
42 sink.put(b"\"");
43 let bytes = value.as_bytes();
44 let mut start = 0usize;
45 for i in 0..bytes.len() {
46 let c = bytes[i];
47 let escape: &[u8] = match c {
48 b'"' => b"\\\"",
49 b'\\' => b"\\\\",
50 0x08 => b"\\b",
51 0x09 => b"\\t",
52 0x0A => b"\\n",
53 0x0C => b"\\f",
54 0x0D => b"\\r",
55 0x00..=0x1F => {
56 if start < i {
57 sink.put(&bytes[start..i]);
58 }
59 let buf = [
60 b'\\',
61 b'u',
62 b'0',
63 b'0',
64 HEX[(c >> 4) as usize],
65 HEX[(c & 0x0F) as usize],
66 ];
67 sink.put(&buf);
68 start = i + 1;
69 continue;
70 }
71 _ => continue,
72 };
73 if start < i {
74 sink.put(&bytes[start..i]);
75 }
76 sink.put(escape);
77 start = i + 1;
78 }
79 if start < bytes.len() {
80 sink.put(&bytes[start..]);
81 }
82 sink.put(b"\"");
83}
84
85fn put_path<S: Sink>(sink: &mut S, path: &[String]) {
87 sink.put(b"[");
88 for (i, seg) in path.iter().enumerate() {
89 if i > 0 {
90 sink.put(b",");
91 }
92 put_json_string(sink, seg);
93 }
94 sink.put(b"]");
95}
96
97fn put_content<S: Sink>(sink: &mut S, content: &BTreeMap<String, BTreeSet<String>>) {
101 sink.put(b"{");
102 for (i, (key, values)) in content.iter().enumerate() {
103 if i > 0 {
104 sink.put(b",");
105 }
106 put_json_string(sink, key);
107 sink.put(b":[");
108 for (j, v) in values.iter().enumerate() {
109 if j > 0 {
110 sink.put(b",");
111 }
112 put_json_string(sink, v);
113 }
114 sink.put(b"]");
115 }
116 sink.put(b"}");
117}
118
119fn to_hex(bytes: &[u8]) -> String {
120 let mut out = String::with_capacity(bytes.len() * 2);
121 for &b in bytes {
122 out.push(HEX[(b >> 4) as usize] as char);
123 out.push(HEX[(b & 0x0F) as usize] as char);
124 }
125 out
126}
127
128pub(crate) fn hash_path(path: &[String]) -> String {
129 let mut h = Sha256::new();
130 put_path(&mut h, path);
131 to_hex(&h.finalize())
132}
133
134pub(crate) fn hash_content(content: &BTreeMap<String, BTreeSet<String>>) -> String {
135 let mut h = Sha256::new();
136 put_content(&mut h, content);
137 to_hex(&h.finalize())
138}
139
140pub fn canonical_json_string(value: &str) -> String {
146 let mut buf = Vec::new();
147 put_json_string(&mut buf, value);
148 String::from_utf8(buf).expect("canonical bytes are valid UTF-8")
149}
150
151pub(crate) fn canonical_path(path: &[String]) -> String {
152 let mut buf = Vec::new();
153 put_path(&mut buf, path);
154 String::from_utf8(buf).expect("canonical bytes are valid UTF-8")
155}
156
157pub(crate) fn canonical_content(content: &BTreeMap<String, BTreeSet<String>>) -> String {
158 let mut buf = Vec::new();
159 put_content(&mut buf, content);
160 String::from_utf8(buf).expect("canonical bytes are valid UTF-8")
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 fn buf_string(value: &str) -> String {
168 let mut v = Vec::new();
169 put_json_string(&mut v, value);
170 String::from_utf8(v).unwrap()
171 }
172
173 #[test]
174 fn json_string_escaping() {
175 assert_eq!(buf_string("plain"), r#""plain""#);
176 assert_eq!(buf_string(r#"a"b"#), r#""a\"b""#);
177 assert_eq!(buf_string(r"a\b"), r#""a\\b""#);
178 assert_eq!(buf_string("tab\there"), r#""tab\there""#);
179 assert_eq!(buf_string("nl\nhere"), r#""nl\nhere""#);
180 assert_eq!(buf_string("\u{0001}"), "\"\\u0001\"");
181 assert_eq!(buf_string("acentuação ✓"), "\"acentuação ✓\""); }
183
184 #[test]
185 fn streaming_and_materialised_paths_agree() {
186 let path = vec!["a".to_string(), "b\"c".to_string()];
188 let materialised = canonical_path(&path);
189 assert_eq!(hash_path(&path), to_hex(&Sha256::digest(materialised.as_bytes())));
190 }
191
192 #[test]
193 fn empty_content_is_empty_object() {
194 assert_eq!(canonical_content(&BTreeMap::new()), "{}");
195 }
196
197 #[test]
198 fn public_canonical_json_string_matches_internal_encoding() {
199 assert_eq!(canonical_json_string(r#"a"b"#), r#""a\"b""#);
200 assert_eq!(canonical_json_string("\u{0001}"), "\"\\u0001\"");
201 assert_eq!(canonical_json_string("acentuação ✓"), "\"acentuação ✓\"");
202 }
203}