nimbus_fml/schema/
hasher.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2* License, v. 2.0. If a copy of the MPL was not distributed with this
3* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use sha2::{Digest, Sha256};
6
7use crate::intermediate_representation::{
8    EnumDef, FeatureDef, ObjectDef, PropDef, TypeRef, VariantDef,
9};
10use std::{
11    collections::{BTreeMap, HashSet},
12    hash::{Hash, Hasher},
13};
14
15use super::TypeQuery;
16
17pub(crate) struct SchemaHasher<'a> {
18    enum_defs: &'a BTreeMap<String, EnumDef>,
19    object_defs: &'a BTreeMap<String, ObjectDef>,
20}
21
22impl<'a> SchemaHasher<'a> {
23    pub(crate) fn new(
24        enums: &'a BTreeMap<String, EnumDef>,
25        objs: &'a BTreeMap<String, ObjectDef>,
26    ) -> Self {
27        Self {
28            enum_defs: enums,
29            object_defs: objs,
30        }
31    }
32
33    pub(crate) fn hash(&self, feature_def: &FeatureDef) -> u64 {
34        let mut hasher: Sha256Hasher = Default::default();
35        feature_def.schema_hash(&mut hasher);
36
37        let types = self.all_types(feature_def);
38
39        // We iterate through the object_defs, then the enum_defs because they are both
40        // ordered, and we want to maintain a stable ordering.
41        // By contrast, `types`, a HashSet, definitely does not have a stable ordering.
42        for (obj_nm, obj_def) in self.object_defs {
43            if types.contains(&TypeRef::Object(obj_nm.clone())) {
44                obj_def.schema_hash(&mut hasher);
45            }
46        }
47
48        for (enum_nm, enum_def) in self.enum_defs {
49            if types.contains(&TypeRef::Enum(enum_nm.clone())) {
50                enum_def.schema_hash(&mut hasher);
51            }
52        }
53
54        hasher.finish()
55    }
56
57    fn all_types(&self, feature_def: &FeatureDef) -> HashSet<TypeRef> {
58        let all_types = TypeQuery::new(self.object_defs);
59        all_types.all_types(feature_def)
60    }
61}
62
63trait SchemaHash {
64    fn schema_hash<H: Hasher>(&self, state: &mut H);
65}
66
67impl SchemaHash for FeatureDef {
68    fn schema_hash<H: Hasher>(&self, state: &mut H) {
69        self.props.schema_hash(state);
70        self.allow_coenrollment.hash(state);
71    }
72}
73
74impl SchemaHash for Vec<PropDef> {
75    fn schema_hash<H: Hasher>(&self, state: &mut H) {
76        let mut vec: Vec<_> = self.iter().collect();
77        vec.sort_by_key(|item| &item.name);
78
79        for item in vec {
80            item.schema_hash(state);
81        }
82    }
83}
84
85impl SchemaHash for Vec<VariantDef> {
86    fn schema_hash<H: Hasher>(&self, state: &mut H) {
87        let mut vec: Vec<_> = self.iter().collect();
88        vec.sort_by_key(|item| &item.name);
89
90        for item in vec {
91            item.schema_hash(state);
92        }
93    }
94}
95
96impl SchemaHash for PropDef {
97    fn schema_hash<H: Hasher>(&self, state: &mut H) {
98        self.name.hash(state);
99        self.typ.hash(state);
100        self.string_alias.hash(state);
101    }
102}
103
104impl SchemaHash for ObjectDef {
105    fn schema_hash<H: Hasher>(&self, state: &mut H) {
106        self.props.schema_hash(state);
107    }
108}
109
110impl SchemaHash for EnumDef {
111    fn schema_hash<H: Hasher>(&self, state: &mut H) {
112        self.variants.schema_hash(state);
113    }
114}
115
116impl SchemaHash for VariantDef {
117    fn schema_hash<H: Hasher>(&self, state: &mut H) {
118        self.name.hash(state);
119    }
120}
121
122#[derive(Default)]
123pub(crate) struct Sha256Hasher {
124    hasher: Sha256,
125}
126
127impl std::hash::Hasher for Sha256Hasher {
128    fn finish(&self) -> u64 {
129        let v = self.hasher.clone().finalize();
130        u64::from_le_bytes(v[0..8].try_into().unwrap())
131    }
132
133    fn write(&mut self, bytes: &[u8]) {
134        self.hasher.update(bytes);
135    }
136}
137
138#[cfg(test)]
139mod unit_tests {
140
141    use crate::error::Result;
142    use serde_json::json;
143
144    use super::*;
145
146    #[test]
147    fn test_simple_schema_is_stable() -> Result<()> {
148        let enums = Default::default();
149        let objs = Default::default();
150
151        let prop1 = PropDef::new("p1", &TypeRef::String, &json!("No"));
152        let prop2 = PropDef::new("p2", &TypeRef::Int, &json!(42));
153
154        let feature_def =
155            FeatureDef::new("test_feature", "documentation", vec![prop1, prop2], false);
156        let mut prev: Option<u64> = None;
157        for _ in 0..100 {
158            let hasher = SchemaHasher::new(&enums, &objs);
159            let hash = hasher.hash(&feature_def);
160            if let Some(prev) = prev {
161                assert_eq!(prev, hash);
162            }
163            prev = Some(hash);
164        }
165
166        Ok(())
167    }
168
169    #[test]
170    fn test_simple_schema_is_stable_with_props_in_any_order() -> Result<()> {
171        let enums = Default::default();
172        let objs = Default::default();
173
174        let prop1 = PropDef::new("p1", &TypeRef::String, &json!("No"));
175        let prop2 = PropDef::new("p2", &TypeRef::Int, &json!(42));
176
177        let f1 = {
178            FeatureDef::new(
179                "test_feature",
180                "documentation",
181                vec![prop1.clone(), prop2.clone()],
182                false,
183            )
184        };
185
186        let f2 = { FeatureDef::new("test_feature", "documentation", vec![prop2, prop1], false) };
187
188        let hasher = SchemaHasher::new(&enums, &objs);
189        assert_eq!(hasher.hash(&f1), hasher.hash(&f2));
190
191        Ok(())
192    }
193
194    #[test]
195    fn test_simple_schema_is_stable_changing_defaults() -> Result<()> {
196        let enums = Default::default();
197        let objs = Default::default();
198
199        let f1 = {
200            let prop1 = PropDef::new("p1", &TypeRef::String, &json!("No"));
201            let prop2 = PropDef::new("p2", &TypeRef::Int, &json!(42));
202            FeatureDef::new("test_feature", "documentation", vec![prop1, prop2], false)
203        };
204
205        let f2 = {
206            let prop1 = PropDef::new("p1", &TypeRef::String, &json!("Nope"));
207            let prop2 = PropDef::new("p2", &TypeRef::Int, &json!(1));
208            FeatureDef::new("test_feature", "documentation", vec![prop1, prop2], false)
209        };
210
211        let hasher = SchemaHasher::new(&enums, &objs);
212        assert_eq!(hasher.hash(&f1), hasher.hash(&f2));
213
214        Ok(())
215    }
216
217    #[test]
218    fn test_simple_schema_is_sensitive_to_change() -> Result<()> {
219        let enums = Default::default();
220        let objs = Default::default();
221
222        let f1 = {
223            let prop1 = PropDef::new("p1", &TypeRef::String, &json!("Nope"));
224            let prop2 = PropDef::new("p2", &TypeRef::Int, &json!(1));
225            FeatureDef::new("test_feature", "documentation", vec![prop1, prop2], false)
226        };
227
228        let hasher = SchemaHasher::new(&enums, &objs);
229
230        // Sensitive to change in type of properties
231        let ne = {
232            let prop1 = PropDef::new("p1", &TypeRef::String, &json!("Nope"));
233            let prop2 = PropDef::new("p2", &TypeRef::Boolean, &json!(1));
234            FeatureDef::new("test_feature", "documentation", vec![prop1, prop2], false)
235        };
236        assert_ne!(hasher.hash(&f1), hasher.hash(&ne));
237
238        // Sensitive to change in name of properties
239        let ne = {
240            let prop1 = PropDef::new("p1_", &TypeRef::String, &json!("Nope"));
241            let prop2 = PropDef::new("p2", &TypeRef::Int, &json!(1));
242            FeatureDef::new("test_feature", "documentation", vec![prop1, prop2], false)
243        };
244        assert_ne!(hasher.hash(&f1), hasher.hash(&ne));
245
246        // Sensitive to change in changes in coenrollment status
247        let ne = {
248            let prop1 = PropDef::new("p1", &TypeRef::String, &json!("Nope"));
249            let prop2 = PropDef::new("p2", &TypeRef::Int, &json!(1));
250            FeatureDef::new("test_feature", "documentation", vec![prop1, prop2], true)
251        };
252        assert_ne!(hasher.hash(&f1), hasher.hash(&ne));
253
254        Ok(())
255    }
256
257    #[test]
258    fn test_schema_is_sensitive_to_enum_change() -> Result<()> {
259        let objs = Default::default();
260
261        let enum_nm = "MyEnum";
262        let enum_t = TypeRef::Enum(enum_nm.to_string());
263
264        let f1 = {
265            let prop1 = PropDef::new("p1", &enum_t, &json!("one"));
266            FeatureDef::new("test_feature", "documentation", vec![prop1], false)
267        };
268
269        let enums = {
270            let enum1 = EnumDef::new(enum_nm, &["one", "two"]);
271            EnumDef::into_map(&[enum1])
272        };
273
274        let hasher = SchemaHasher::new(&enums, &objs);
275        let h1 = hasher.hash(&f1);
276
277        let enums = {
278            let enum1 = EnumDef::new(enum_nm, &["one", "two", "newly-added"]);
279            EnumDef::into_map(&[enum1])
280        };
281        let hasher = SchemaHasher::new(&enums, &objs);
282        let ne = hasher.hash(&f1);
283
284        assert_ne!(h1, ne);
285
286        Ok(())
287    }
288
289    #[test]
290    fn test_schema_is_sensitive_only_to_the_enums_used() -> Result<()> {
291        let objs = Default::default();
292
293        let enum_nm = "MyEnum";
294        let enum_t = TypeRef::Enum(enum_nm.to_string());
295
296        let f1 = {
297            let prop1 = PropDef::new("p1", &enum_t, &json!("one"));
298            FeatureDef::new("test_feature", "documentation", vec![prop1], false)
299        };
300
301        let enums = {
302            let enum1 = EnumDef::new(enum_nm, &["one", "two"]);
303            let enums1 = &[enum1];
304            EnumDef::into_map(enums1)
305        };
306
307        let hasher = SchemaHasher::new(&enums, &objs);
308        // Get an original hash here.
309        let h1 = hasher.hash(&f1);
310
311        let enums = {
312            let enum1 = EnumDef::new(enum_nm, &["one", "two"]);
313            // Add an extra enum here.
314            let enum2 = EnumDef::new("AnotherEnum", &["one", "two"]);
315            let enums1 = &[enum1, enum2];
316            EnumDef::into_map(enums1)
317        };
318        let hasher = SchemaHasher::new(&enums, &objs);
319        let h2 = hasher.hash(&f1);
320
321        assert_eq!(h1, h2);
322
323        Ok(())
324    }
325
326    #[test]
327    fn test_schema_is_sensitive_to_object_change() -> Result<()> {
328        let enums = Default::default();
329        let obj_nm = "MyObject";
330        let obj_t = TypeRef::Object(obj_nm.to_string());
331
332        let f1 = {
333            let prop1 = PropDef::new("p1", &obj_t, &json!({}));
334            FeatureDef::new("test_feature", "documentation", vec![prop1], false)
335        };
336
337        let objs = {
338            let obj_def = ObjectDef::new(
339                obj_nm,
340                &[PropDef::new("obj-p1", &TypeRef::Boolean, &json!(true))],
341            );
342
343            ObjectDef::into_map(&[obj_def])
344        };
345
346        let hasher = SchemaHasher::new(&enums, &objs);
347        // Get an original hash here.
348        let h1 = hasher.hash(&f1);
349
350        let objs = {
351            let obj_def = ObjectDef::new(
352                obj_nm,
353                &[
354                    PropDef::new("obj-p1", &TypeRef::Boolean, &json!(true)),
355                    PropDef::new("obj-p2", &TypeRef::Boolean, &json!(true)),
356                ],
357            );
358
359            ObjectDef::into_map(&[obj_def])
360        };
361
362        let hasher = SchemaHasher::new(&enums, &objs);
363        let ne = hasher.hash(&f1);
364
365        assert_ne!(h1, ne);
366
367        Ok(())
368    }
369
370    #[test]
371    fn test_schema_is_sensitive_only_to_the_objects_used() -> Result<()> {
372        let enums = Default::default();
373
374        let obj_nm = "MyObject";
375        let obj_t = TypeRef::Object(obj_nm.to_string());
376
377        let f1 = {
378            let prop1 = PropDef::new("p1", &obj_t, &json!({}));
379            FeatureDef::new("test_feature", "documentation", vec![prop1], false)
380        };
381
382        let objects = {
383            let obj1 = ObjectDef::new(
384                obj_nm,
385                &[PropDef::new("obj-p1", &TypeRef::Boolean, &json!(true))],
386            );
387            ObjectDef::into_map(&[obj1])
388        };
389
390        let hasher = SchemaHasher::new(&enums, &objects);
391        // Get an original hash here.
392        let h1 = hasher.hash(&f1);
393
394        // Now add more objects, that aren't related to this feature.
395        let objects = {
396            let obj1 = ObjectDef::new(
397                obj_nm,
398                &[PropDef::new("obj-p1", &TypeRef::Boolean, &json!(true))],
399            );
400            let obj2 = ObjectDef::new(
401                "AnotherObject",
402                &[PropDef::new("obj-p1", &TypeRef::Boolean, &json!(true))],
403            );
404            ObjectDef::into_map(&[obj1, obj2])
405        };
406
407        let hasher = SchemaHasher::new(&enums, &objects);
408        let h2 = hasher.hash(&f1);
409
410        assert_eq!(h1, h2);
411
412        Ok(())
413    }
414
415    #[test]
416    fn test_schema_is_sensitive_to_nested_change() -> Result<()> {
417        let obj_nm = "MyObject";
418        let obj_t = TypeRef::Object(obj_nm.to_string());
419
420        let enum_nm = "MyEnum";
421        let enum_t = TypeRef::Enum(enum_nm.to_string());
422
423        let f1 = {
424            let prop1 = PropDef::new("p1", &obj_t, &json!({}));
425            FeatureDef::new("test_feature", "documentation", vec![prop1], false)
426        };
427
428        let objs = {
429            let obj_def = ObjectDef::new(obj_nm, &[PropDef::new("obj-p1", &enum_t, &json!("one"))]);
430
431            ObjectDef::into_map(&[obj_def])
432        };
433
434        let enums = {
435            let enum1 = EnumDef::new(enum_nm, &["one", "two"]);
436            EnumDef::into_map(&[enum1])
437        };
438
439        let hasher = SchemaHasher::new(&enums, &objs);
440        // Get an original hash here.
441        let h1 = hasher.hash(&f1);
442
443        // Now change a deeply nested enum variant.
444        let enums = {
445            let enum1 = EnumDef::new(enum_nm, &["one", "two", "newly-added"]);
446            EnumDef::into_map(&[enum1])
447        };
448        let hasher = SchemaHasher::new(&enums, &objs);
449        let ne = hasher.hash(&f1);
450
451        assert_ne!(h1, ne);
452        Ok(())
453    }
454}