Skip to main content

system_image/
path_hash_mapping.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::errors::PathHashMappingError;
6use buf_read_ext::BufReadExt as _;
7use fuchsia_hash::Hash;
8use fuchsia_pkg::PackagePath;
9use sorted_vec_map::SortedVecMap;
10use std::io;
11use std::marker::PhantomData;
12use std::str::FromStr as _;
13
14/// PhantomData type marker to indicate a `PathHashMapping` is a "data/static_packages" file.
15#[derive(Debug, PartialEq, Eq, Clone, Copy)]
16pub struct Static;
17
18/// PhantomData type marker to indicate a `PathHashMapping` is a "data/bootfs_packages" file.
19#[derive(Debug, PartialEq, Eq, Clone, Copy)]
20pub struct Bootfs;
21
22pub type StaticPackages = PathHashMapping<Static>;
23
24/// A `PathHashMapping` reads and writes line-oriented "{package_path}={hash}\n" files, e.g.
25/// "data/static_packages".
26/// Deprecated.
27#[derive(Debug, PartialEq, Eq)]
28pub struct PathHashMapping<T> {
29    contents: SortedVecMap<PackagePath, Hash>,
30    phantom: PhantomData<T>,
31}
32
33impl<T> PathHashMapping<T> {
34    /// Reads the line-oriented "package-path=hash" static_packages or cache_packages file.
35    /// Validates the package paths and hashes.
36    pub fn deserialize(mut reader: impl io::BufRead) -> Result<Self, PathHashMappingError> {
37        let mut contents = SortedVecMap::builder();
38        let mut lines = reader.lending_lines();
39        while let Some(line) = lines.next() {
40            let line = line?;
41            let i = line.rfind('=').ok_or_else(|| PathHashMappingError::EntryHasNoEqualsSign {
42                entry: line.to_owned(),
43            })?;
44            let hash = Hash::from_str(&line[i + 1..])?;
45            let path = line[..i].parse()?;
46            contents.insert(path, hash);
47        }
48        Ok(Self { contents: contents.build(), phantom: PhantomData })
49    }
50
51    /// Iterator over the contents of the mapping.
52    pub fn contents(&self) -> &SortedVecMap<PackagePath, Hash> {
53        &self.contents
54    }
55
56    /// Iterator over the contents of the mapping, consuming self.
57    pub fn into_contents(self) -> SortedVecMap<PackagePath, Hash> {
58        self.contents
59    }
60
61    /// Iterator over the contained hashes.
62    pub fn hashes(&self) -> impl Iterator<Item = &Hash> {
63        self.contents.values()
64    }
65
66    /// Get the hash for a package.
67    pub fn hash_for_package(&self, path: &PackagePath) -> Option<Hash> {
68        self.contents.get(path).copied()
69    }
70
71    /// Shrinks the capacity of the mapping as much as possible.
72    pub fn shrink_to_fit(&mut self) {
73        self.contents.shrink_to_fit();
74    }
75
76    /// Write a `static_packages` or `cache_packages` file.
77    pub fn serialize(&self, mut writer: impl io::Write) -> Result<(), PathHashMappingError> {
78        for (k, v) in &self.contents {
79            writeln!(&mut writer, "{}={}", k, v)?;
80        }
81        Ok(())
82    }
83}
84
85impl<T> FromIterator<(PackagePath, Hash)> for PathHashMapping<T> {
86    fn from_iter<I: IntoIterator<Item = (PackagePath, Hash)>>(iter: I) -> Self {
87        let contents = SortedVecMap::from_iter(iter);
88        Self { contents, phantom: PhantomData }
89    }
90}
91
92impl<T, const N: usize> From<[(PackagePath, Hash); N]> for PathHashMapping<T> {
93    fn from(entries: [(PackagePath, Hash); N]) -> Self {
94        let contents = SortedVecMap::from_iter(entries);
95        Self { contents, phantom: PhantomData }
96    }
97}
98
99impl<T> IntoIterator for PathHashMapping<T> {
100    type Item = (PackagePath, Hash);
101    type IntoIter = std::vec::IntoIter<(PackagePath, Hash)>;
102
103    fn into_iter(self) -> Self::IntoIter {
104        self.contents.into_iter()
105    }
106}
107
108impl<'a, T> IntoIterator for &'a PathHashMapping<T> {
109    type Item = (&'a PackagePath, &'a Hash);
110    type IntoIter = sorted_vec_map::sorted_vec_map::Iter<'a, PackagePath, Hash>;
111
112    fn into_iter(self) -> Self::IntoIter {
113        (&self.contents).into_iter()
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use assert_matches::assert_matches;
121    use fuchsia_pkg::test::random_package_path;
122    use proptest::prelude::*;
123
124    #[test]
125    fn deserialize_empty_file() {
126        let empty = Vec::new();
127        let static_packages = StaticPackages::deserialize(empty.as_slice()).unwrap();
128        assert_eq!(static_packages.hashes().count(), 0);
129    }
130
131    #[test]
132    fn deserialize_valid_file_list_hashes() {
133        let bytes =
134            "name/variant=0000000000000000000000000000000000000000000000000000000000000000\n\
135             other-name/other-variant=1111111111111111111111111111111111111111111111111111111111111111\n"
136                .as_bytes();
137        let static_packages = StaticPackages::deserialize(bytes).unwrap();
138        assert_eq!(
139            static_packages.hashes().cloned().collect::<Vec<_>>(),
140            vec![
141                "0000000000000000000000000000000000000000000000000000000000000000".parse().unwrap(),
142                "1111111111111111111111111111111111111111111111111111111111111111".parse().unwrap()
143            ]
144        );
145    }
146
147    #[test]
148    fn deserialze_rejects_invalid_package_path() {
149        let bytes =
150            "name/=0000000000000000000000000000000000000000000000000000000000000000\n".as_bytes();
151        let res = StaticPackages::deserialize(bytes);
152        assert_matches!(res, Err(PathHashMappingError::ParsePackagePath(_)));
153    }
154
155    #[test]
156    fn deserialize_rejects_invalid_hash() {
157        let bytes = "name/variant=invalid-hash\n".as_bytes();
158        let res = StaticPackages::deserialize(bytes);
159        assert_matches!(res, Err(PathHashMappingError::ParseHash(_)));
160    }
161
162    #[test]
163    fn deserialize_rejects_missing_equals() {
164        let bytes =
165            "name/variant~0000000000000000000000000000000000000000000000000000000000000000\n"
166                .as_bytes();
167        let res = StaticPackages::deserialize(bytes);
168        assert_matches!(res, Err(PathHashMappingError::EntryHasNoEqualsSign { .. }));
169    }
170
171    #[test]
172    fn from_serialize() {
173        let static_packages = StaticPackages::from([(
174            PackagePath::from_name_and_variant("name0".parse().unwrap(), "0".parse().unwrap()),
175            "0000000000000000000000000000000000000000000000000000000000000000".parse().unwrap(),
176        )]);
177
178        let mut serialized = vec![];
179        static_packages.serialize(&mut serialized).unwrap();
180        assert_eq!(
181            serialized,
182            &b"name0/0=0000000000000000000000000000000000000000000000000000000000000000\n"[..]
183        );
184    }
185
186    #[test]
187    fn hash_for_package_success() {
188        let bytes =
189            "name/variant=0000000000000000000000000000000000000000000000000000000000000000\n\
190             "
191            .as_bytes();
192        let static_packages = StaticPackages::deserialize(bytes).unwrap();
193        let res = static_packages.hash_for_package(&PackagePath::from_name_and_variant(
194            "name".parse().unwrap(),
195            "variant".parse().unwrap(),
196        ));
197        assert_eq!(
198            res,
199            Some(
200                "0000000000000000000000000000000000000000000000000000000000000000".parse().unwrap(),
201            )
202        );
203    }
204
205    #[test]
206    fn hash_for_missing_package_is_none() {
207        let bytes =
208            "name/variant=0000000000000000000000000000000000000000000000000000000000000000\n\
209             "
210            .as_bytes();
211        let static_packages = StaticPackages::deserialize(bytes).unwrap();
212        let res = static_packages.hash_for_package(&PackagePath::from_name_and_variant(
213            "nope".parse().unwrap(),
214            "variant".parse().unwrap(),
215        ));
216        assert_eq!(res, None);
217    }
218
219    prop_compose! {
220        fn random_hash()(s in "[A-Fa-f0-9]{64}") -> Hash {
221            s.parse().unwrap()
222        }
223    }
224
225    prop_compose! {
226        fn random_static_packages()
227            (vec in prop::collection::vec(
228                (random_package_path(), random_hash()), 0..4)
229            ) -> PathHashMapping<Static> {
230                StaticPackages::from_iter(vec)
231            }
232    }
233
234    proptest! {
235        #[test]
236        fn serialize_deserialize_identity(static_packages in random_static_packages()) {
237            let mut serialized = vec![];
238            static_packages.serialize(&mut serialized).unwrap();
239            let deserialized = StaticPackages::deserialize(serialized.as_slice()).unwrap();
240            prop_assert_eq!(
241                static_packages,
242                deserialized
243            );
244        }
245    }
246}