Skip to main content

shuyi_assets/bundle/
fingerprint.rs

1//! A bundle's fingerprint ([`Bundle::fingerprint`]).
2
3use shuyi_core::ContentHash;
4
5use super::Bundle;
6use crate::fnv::Hasher;
7use crate::prelude::*;
8
9impl Bundle {
10    /// A fingerprint of the content that ships, the same on every
11    /// platform: what a record or a save is stamped with
12    /// (`shuyi_core::Made`). Any byte, name or file changed in a layer
13    /// that ships changes it; the test layers never count, whatever the
14    /// thread ([`story_only`](super::story_only) changes nothing).
15    ///
16    /// The layers that ship are taken from the last read to the first
17    /// (the base layer first), each file of a layer in path order, as its
18    /// path's length and bytes then its contents' length and bytes, into
19    /// the word-wise FNV-1a hash of the pack's checksum. A layer that
20    /// [stands for](super::Layer::standing_for) a bundle (a pack's)
21    /// replaces what was taken so far with that bundle's fingerprint. So a
22    /// build that ships one pack has the fingerprint of the files it was
23    /// baked from, and a layer laid over it counts as it would over them.
24    ///
25    /// It reads every byte that ships, without telling the
26    /// [`watch`](super::watch)er: compute it once and keep it.
27    pub fn fingerprint(&self) -> ContentHash {
28        let mut hash = Hasher::new();
29        for layer in self.shipped().iter().rev() {
30            if let Some(content) = layer.stands_for {
31                hash = Hasher(content.0);
32                continue;
33            }
34            let mut paths = Vec::new();
35            layer.files.list("", true, &mut paths);
36            paths.sort_unstable();
37            for path in paths {
38                hash.sized(path.as_bytes());
39                hash.sized(layer.files.get(path).unwrap_or_default());
40            }
41        }
42        ContentHash(hash.0)
43    }
44}
45
46#[cfg(test)]
47pub(crate) mod tests {
48    use alloc::collections::BTreeMap;
49
50    use super::super::{Layer, story_only};
51    use super::*;
52
53    static BASE: &[(&str, &[u8])] = &[
54        ("data/a.ron", b"base a"),
55        ("data/b.ron", b"base b"),
56        ("top.txt", b"top"),
57    ];
58
59    static OVER: &[(&str, &[u8])] = &[("data/a.ron", b"over a"), ("new.txt", b"new")];
60
61    /// A layer of `files`, kept for the run.
62    fn layer(files: &[(&str, &[u8])]) -> Layer {
63        let files = files
64            .iter()
65            .map(|(path, bytes)| ((*path).to_owned(), bytes.to_vec()))
66            .collect::<BTreeMap<_, _>>();
67        Layer::owned("layer".to_owned(), files).unwrap()
68    }
69
70    /// The fingerprint of one layer of `files`.
71    fn of(files: &[(&str, &[u8])]) -> ContentHash {
72        Bundle::new(vec![layer(files)]).fingerprint()
73    }
74
75    /// A fixed bundle of two layers. Its fingerprint is the row
76    /// `fingerprint` of `tests/golden/formats.ron` (ticket 2116): a change
77    /// there changes every stamp a game wrote, whose records then read as
78    /// made with other content.
79    pub(crate) fn fixed() -> Bundle {
80        Bundle::new(vec![Layer::table("over", OVER), Layer::table("base", BASE)])
81    }
82
83    #[test]
84    fn no_layer_is_the_offset_basis() {
85        // No layer, or an empty one, is FNV-1a's offset basis.
86        assert_eq!(
87            Bundle::new(Vec::new()).fingerprint(),
88            ContentHash(0xcbf2_9ce4_8422_2325)
89        );
90        assert_eq!(of(&[]), ContentHash(0xcbf2_9ce4_8422_2325));
91    }
92
93    #[test]
94    fn a_byte_a_name_or_a_file_changed_changes_it() {
95        let base = of(BASE);
96        assert_eq!(of(BASE), base);
97        let byte: &[(&str, &[u8])] = &[
98            ("data/a.ron", b"base A"),
99            ("data/b.ron", b"base b"),
100            ("top.txt", b"top"),
101        ];
102        let name: &[(&str, &[u8])] = &[
103            ("data/a.ron", b"base a"),
104            ("data/c.ron", b"base b"),
105            ("top.txt", b"top"),
106        ];
107        let added: &[(&str, &[u8])] = &[
108            ("data/a.ron", b"base a"),
109            ("data/b.ron", b"base b"),
110            ("data/c.ron", b""),
111            ("top.txt", b"top"),
112        ];
113        // A byte moved from a path to its contents.
114        let moved: &[(&str, &[u8])] = &[
115            ("data/a.ro", b"nbase a"),
116            ("data/b.ron", b"base b"),
117            ("top.txt", b"top"),
118        ];
119        for changed in [byte, name, added, moved] {
120            assert_ne!(of(changed), base, "{changed:?}");
121        }
122    }
123
124    #[test]
125    fn the_layers_count_base_first_and_the_test_layers_never() {
126        let base = Layer::table("base", BASE);
127        let over = Layer::table("over", OVER);
128        let both = Bundle::new(vec![over, base]).fingerprint();
129        assert_ne!(both, Bundle::new(vec![base, over]).fingerprint());
130        assert_ne!(both, of(BASE));
131        // A test layer, read or not, counts for nothing.
132        let tests = Bundle::new(vec![over, base, Layer::table("tests", BASE).test()]);
133        assert_eq!(tests.fingerprint(), both);
134        assert_eq!(story_only(|| tests.fingerprint()), both);
135        let changed = Bundle::new(vec![over, base, Layer::table("tests", OVER).test()]);
136        assert_eq!(changed.fingerprint(), both);
137        // A private layer ships, and counts.
138        let private = Bundle::new(vec![over.private(), base]).fingerprint();
139        assert_eq!(private, both);
140        // Whatever kind of layer holds the files, they count the same.
141        assert_eq!(
142            Bundle::new(vec![layer(OVER), layer(BASE)]).fingerprint(),
143            both
144        );
145    }
146
147    #[test]
148    fn a_layer_standing_for_a_bundle_takes_its_fingerprint_and_hides_those_under_it() {
149        let base = Layer::table("base", BASE);
150        let over = Layer::table("over", OVER);
151        let loose = Bundle::new(vec![base]).fingerprint();
152        // A pack of `BASE` alone: its own files don't count.
153        let pack = Layer::table("pack", OVER).standing_for(loose);
154        assert_eq!(Bundle::new(vec![pack]).fingerprint(), loose);
155        // A loose layer over it counts as over the files it stands for.
156        assert_eq!(
157            Bundle::new(vec![over, pack]).fingerprint(),
158            Bundle::new(vec![over, base]).fingerprint()
159        );
160        // A layer under it doesn't count: the pack holds every file.
161        assert_eq!(
162            Bundle::new(vec![pack, over]).fingerprint(),
163            Bundle::new(vec![pack]).fingerprint()
164        );
165        // A test layer standing for something counts for nothing either.
166        let test = Layer::table("tests", OVER)
167            .standing_for(ContentHash(1))
168            .test();
169        assert_eq!(Bundle::new(vec![base, test]).fingerprint(), loose);
170    }
171
172    proptest::proptest! {
173        /// One byte changed in one file of any bundle changes its
174        /// fingerprint.
175        #[test]
176        fn one_byte_changed_anywhere_changes_it(
177            layers in proptest::collection::vec(
178                proptest::collection::btree_map(
179                    "[ab]{1,2}(/[ab]{1,2}){0,2}",
180                    proptest::collection::vec(proptest::num::u8::ANY, 1..20),
181                    1..6,
182                ),
183                1..4,
184            ),
185            which in proptest::num::usize::ANY,
186            at in proptest::num::usize::ANY,
187            flip in 1..=u8::MAX,
188        ) {
189            let make = |layers: &[BTreeMap<String, Vec<u8>>]| {
190                let layers = layers
191                    .iter()
192                    .enumerate()
193                    .map(|(i, files)| Layer::owned(format!("layer{i}"), files.clone()).unwrap())
194                    .collect();
195                Bundle::new(layers).fingerprint()
196            };
197            let before = make(&layers);
198            let mut changed = layers.clone();
199            let files: Vec<(usize, String)> = changed
200                .iter()
201                .enumerate()
202                .flat_map(|(i, files)| files.keys().map(move |path| (i, path.clone())))
203                .collect();
204            let (layer, path) = &files[which % files.len()];
205            let bytes = changed[*layer].get_mut(path).unwrap();
206            let len = bytes.len();
207            bytes[at % len] ^= flip;
208            proptest::prop_assert_ne!(make(&changed), before);
209        }
210    }
211}