harmos_domain/schema/
identity.rs1use sha2::{Digest as _, Sha256};
22use super::Cardinality;
24use super::graph::{Error, Kind, View};
25use crate::Fingerprint;
26
27pub fn fingerprint(root: View<'_>, codec: &str) -> Result<Fingerprint, Error> {
44 let canonical = canonical(root)?;
45 let mut hash = Sha256::new();
46 hash.update(b"harmos.schema.v1\0");
47 hash.update(u32::try_from(codec.len()).map_err(|_| Error::Budget)?.to_le_bytes());
48 hash.update(codec.as_bytes());
49 hash.update(canonical);
50 Ok(Fingerprint(hash.finalize().into()))
51}
52
53pub fn contract(id: &str, schema: &super::graph::Schema, codec: &str) -> Result<Fingerprint, Error> {
54 let root = schema.root()?;
55 let direction = |name: &str| -> Result<Option<Fingerprint>, Error> {
56 let Some(field) = root.fields().iter().find(|field| field.name == name) else {
57 return Ok(None);
58 };
59 let value = root.message(field)?.ok_or_else(|| Error::Field {
60 field: name.to_owned(),
61 reason: "expected a message direction",
62 })?;
63 fingerprint(value, codec).map(Some)
64 };
65 let request = direction("request")?.ok_or_else(|| Error::Field {
66 field: "request".into(),
67 reason: "missing request",
68 })?;
69 let response = direction("response")?.ok_or_else(|| Error::Field {
70 field: "response".into(),
71 reason: "missing response",
72 })?;
73 Ok(operation(id, request, response, direction("input")?, direction("output")?))
74}
75
76pub fn operation(
77 id: &str, request: Fingerprint, response: Fingerprint, input: Option<Fingerprint>, output: Option<Fingerprint>,
78) -> Fingerprint {
79 let mut hash = Sha256::new();
80 hash.update(b"harmos.operation.v1\0");
81 hash.update(u32::try_from(id.len()).expect("validated operation id length").to_le_bytes());
82 hash.update(id.as_bytes());
83 hash.update(request.0);
84 hash.update(response.0);
85 for direction in [input, output] {
86 hash.update([u8::from(direction.is_some())]);
87 if let Some(fingerprint) = direction {
88 hash.update(fingerprint.0);
89 }
90 }
91 Fingerprint(hash.finalize().into())
92}
93
94pub fn canonical(root: View<'_>) -> Result<Vec<u8>, Error> {
95 let schema = root.schema();
96 schema.prepare(&crate::config::SchemaSettings::default())?;
97 let mut indices = vec![None; schema.definitions.len()];
98 let mut order = Vec::new();
99 let mut pending = vec![root.index()];
100 while let Some(index) = pending.pop() {
101 if indices[index].is_some() {
102 continue;
103 }
104 indices[index] = Some(u32::try_from(order.len()).map_err(|_| Error::Budget)?);
105 order.push(index);
106 for field in schema.definitions[index].iter().rev() {
107 if let Kind::Message { definition } = field.kind {
108 pending.push(definition);
109 }
110 }
111 }
112
113 let mut bytes = Vec::new();
114 bytes.extend_from_slice(&u32::try_from(order.len()).map_err(|_| Error::Budget)?.to_le_bytes());
115 for index in order {
116 let fields = &schema.definitions[index];
117 bytes.extend_from_slice(&u32::try_from(fields.len()).map_err(|_| Error::Budget)?.to_le_bytes());
118 for field in fields {
119 bytes.extend_from_slice(&u32::try_from(field.name.len()).map_err(|_| Error::Budget)?.to_le_bytes());
120 bytes.extend_from_slice(field.name.as_bytes());
121 bytes.extend_from_slice(&field.tag.to_le_bytes());
122 bytes.push(match field.kind {
123 Kind::Bool => 0,
124 Kind::U32 => 1,
125 Kind::U64 => 2,
126 Kind::I32 => 3,
127 Kind::I64 => 4,
128 Kind::F32 => 5,
129 Kind::F64 => 6,
130 Kind::String => 7,
131 Kind::Bytes => 8,
132 Kind::Message { .. } => 9,
133 });
134 bytes.push(match field.cardinality {
135 Cardinality::Required => 0,
136 Cardinality::Optional => 1,
137 Cardinality::Repeated => 2,
138 Cardinality::Variant => 3,
139 });
140 if let Kind::Message { definition } = field.kind {
141 bytes.extend_from_slice(&indices[definition].expect("visited schema edge").to_le_bytes());
142 }
143 }
144 }
145 Ok(bytes)
146}
147
148#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::Portable;
156 use crate::schema::graph::{Field, Schema};
157
158 #[test]
159 fn recursive_graph_vector() {
160 let first = vec![
161 Field {
162 name: "child".into(),
163 tag: 1,
164 kind: Kind::Message { definition: 1 },
165 cardinality: Cardinality::Optional,
166 },
167 Field {
168 name: "again".into(),
169 tag: 2,
170 kind: Kind::Message { definition: 1 },
171 cardinality: Cardinality::Required,
172 },
173 ];
174 let second = vec![Field {
175 name: "back".into(),
176 tag: 1,
177 kind: Kind::Message { definition: 0 },
178 cardinality: Cardinality::Optional,
179 }];
180 let schema = Schema {
181 root: 0,
182 definitions: vec![first, second],
183 };
184 let expected = fingerprint(schema.root().unwrap(), "harmos.postcard.v1").unwrap();
185 assert_eq!(expected.to_string(), "c3dc086aeed6d067ea637e8648be9a969ce4ba40f4b6bd648e785dd06f6523fb");
186 let mut reordered = schema.clone();
187 reordered.root = 1;
188 reordered.definitions.swap(0, 1);
189 for fields in &mut reordered.definitions {
190 for field in fields {
191 if let Kind::Message { definition } = &mut field.kind {
192 *definition = 1 - *definition;
193 }
194 }
195 }
196 assert_eq!(fingerprint(reordered.root().unwrap(), "harmos.postcard.v1").unwrap(), expected);
197 assert_ne!(fingerprint(schema.root().unwrap(), "custom.v1").unwrap(), expected);
198 reordered.definitions.push(Vec::new());
199 assert!(canonical(reordered.root().unwrap()).is_err());
200 }
201
202 #[test]
203 fn stream_operation_vector() {
204 let request = bool::fingerprint();
205 let response = u32::fingerprint();
206 let output = Some(f64::fingerprint());
207 assert_eq!(request.to_string(), "20033d99f99ca5f13e227f98e440bde179835de34d6de2b56c5cf022fd0b2d56");
208 assert_eq!(response.to_string(), "9f9059e4406a95ab693a629ca73e030046ebb86101ff8492544651489706ec4f");
209 let identity = operation("read", request, response, None, output);
210 assert_eq!(identity.to_string(), "d1c8efd88462da38b06fbb162e9ffc3a430b645f51934d8a0e3285fb3fb2e74d");
211 assert_ne!(identity, operation("read", request, response, output, None));
212 assert_ne!(identity, operation("read", request, response, None, None));
213 }
214}