1use sha2::{Digest, Sha256};
11
12#[derive(Debug)]
17pub struct CanonicalEncoder {
18 out: Vec<u8>,
19}
20
21impl CanonicalEncoder {
22 pub fn new(domain_tag: &[u8]) -> Self {
25 let mut encoder = Self { out: Vec::new() };
26 encoder.field(domain_tag);
27 encoder
28 }
29
30 pub fn field(&mut self, bytes: &[u8]) -> &mut Self {
32 self.out
33 .extend_from_slice(&(bytes.len() as u64).to_be_bytes());
34 self.out.extend_from_slice(bytes);
35 self
36 }
37
38 pub fn text(&mut self, value: &str) -> &mut Self {
40 self.field(value.as_bytes())
41 }
42
43 pub fn u64(&mut self, value: u64) -> &mut Self {
45 self.out.extend_from_slice(&value.to_be_bytes());
46 self
47 }
48
49 pub fn bool(&mut self, value: bool) -> &mut Self {
51 self.out.push(u8::from(value));
52 self
53 }
54
55 pub fn list<I>(&mut self, entries: I) -> &mut Self
57 where
58 I: ExactSizeIterator,
59 I::Item: AsRef<str>,
60 {
61 self.out
62 .extend_from_slice(&(entries.len() as u64).to_be_bytes());
63 for entry in entries {
64 self.text(entry.as_ref());
65 }
66 self
67 }
68
69 pub fn finish(self) -> Vec<u8> {
71 self.out
72 }
73
74 pub fn digest(self) -> String {
77 let mut hasher = Sha256::new();
78 hasher.update(&self.out);
79 let mut hex = String::with_capacity(7 + 64);
80 hex.push_str("sha256:");
81 for byte in hasher.finalize() {
82 hex.push_str(&format!("{byte:02x}"));
83 }
84 hex
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn encoding_is_length_prefixed_and_unambiguous() {
94 let mut a = CanonicalEncoder::new(b"tag");
95 a.text("ab").text("c");
96 let mut b = CanonicalEncoder::new(b"tag");
97 b.text("a").text("bc");
98 assert_ne!(a.finish(), b.finish(), "field boundaries must not alias");
99 }
100
101 #[test]
102 fn lists_are_count_prefixed() {
103 let mut a = CanonicalEncoder::new(b"tag");
104 a.list(["x", "y"].iter());
105 let mut b = CanonicalEncoder::new(b"tag");
106 b.list(["x"].iter()).list(["y"].iter());
107 assert_ne!(a.finish(), b.finish());
108 }
109
110 #[test]
111 fn digest_is_deterministic_and_self_describing() {
112 let mut a = CanonicalEncoder::new(b"perspt-prompt-v1");
113 a.text("section").u64(3).bool(true);
114 let mut b = CanonicalEncoder::new(b"perspt-prompt-v1");
115 b.text("section").u64(3).bool(true);
116 let digest = a.digest();
117 assert_eq!(digest, b.digest());
118 assert!(digest.starts_with("sha256:"));
119 assert_eq!(digest.len(), 7 + 64);
120 }
121}