Skip to main content

harmos_domain/schema/
identity.rs

1/* ***
2 *
3 * Harmos
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * ***
8 */
9
10// Canonical Schema Traversal And Structural Identity
11
12/* === Modules === */
13
14/* === Re-Exports === */
15
16/* === Imports === */
17
18// Standard Library Imports
19
20// Third Party Imports
21use sha2::{Digest as _, Sha256};
22// Local Imports
23use super::Cardinality;
24use super::graph::{Error, Kind, View};
25use crate::Fingerprint;
26
27/* === Constants === */
28
29/* === Macros === */
30
31/* === Type Aliases === */
32
33/* === Structs === */
34
35/* === Enums === */
36
37/* === Traits === */
38
39/* === Implementations === */
40
41/* === Public Functions === */
42
43pub 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/* === Private Functions === */
149
150/* === Tests === */
151
152#[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}