Skip to main content

fuchsia_pkg/
meta_contents.rs

1// Copyright 2019 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::MetaContentsError;
6use fuchsia_merkle::Hash;
7use std::collections::HashMap;
8use std::io;
9use std::str::FromStr;
10
11/// A `MetaContents` represents the "meta/contents" file of a Fuchsia archive
12/// file of a Fuchsia package.
13/// It validates that all resource paths are valid and that none of them start
14/// with "meta/".
15#[derive(Debug, PartialEq, Eq, Clone)]
16pub struct MetaContents {
17    contents: HashMap<String, Hash>,
18}
19
20impl MetaContents {
21    pub const PATH: &'static str = "meta/contents";
22
23    /// Creates a `MetaContents` from a `map` from resource paths to Merkle roots.
24    /// Validates that:
25    ///   1. all resource paths are valid Fuchsia package resource paths,
26    ///   2. none of the resource paths start with "meta/",
27    ///   3. none of the resource paths are "meta",
28    ///   4. none of the resource paths have directories that collide with other full resource
29    ///      paths, e.g. path combination ["foo", "foo/bar"] would be rejected because it has
30    ///      both a file and a directory at path "foo".
31    ///
32    /// # Examples
33    /// ```
34    /// # use fuchsia_merkle::Hash;
35    /// # use fuchsia_pkg::MetaContents;
36    /// # use std::collections::HashMap;
37    /// # use std::str::FromStr;
38    /// let map = HashMap::from([
39    ///     ("bin/my_prog".to_string(),
40    ///         Hash::from_str(
41    ///             "0000000000000000000000000000000000000000000000000000000000000000")
42    ///         .unwrap()),
43    ///     ("lib/mylib.so".to_string(),
44    ///         Hash::from_str(
45    ///             "1111111111111111111111111111111111111111111111111111111111111111")
46    ///         .unwrap()),
47    /// ]);
48    /// let meta_contents = MetaContents::from_map(map).unwrap();
49    pub fn from_map(map: HashMap<String, Hash>) -> Result<Self, MetaContentsError> {
50        for resource_path in map.keys() {
51            fuchsia_url::Resource::validate_str(resource_path).map_err(|e| {
52                MetaContentsError::InvalidResourcePath { cause: e, path: resource_path.to_string() }
53            })?;
54            if resource_path.starts_with("meta/") || resource_path == "meta" {
55                return Err(MetaContentsError::ExternalContentInMetaDirectory {
56                    path: resource_path.to_string(),
57                });
58            }
59            for (i, _) in resource_path.match_indices('/') {
60                if map.contains_key(&resource_path[..i]) {
61                    return Err(MetaContentsError::FileDirectoryCollision {
62                        path: resource_path[..i].to_string(),
63                    });
64                }
65            }
66        }
67        Ok(MetaContents { contents: map })
68    }
69
70    /// Serializes a "meta/contents" file to `writer`.
71    ///
72    /// # Examples
73    /// ```
74    /// # use fuchsia_merkle::Hash;
75    /// # use fuchsia_pkg::MetaContents;
76    /// # use std::collections::HashMap;
77    /// # use std::str::FromStr;
78    /// let map: PackedMap<str, Hash> = HashMap::from([
79    ///     ("bin/my_prog".to_string(),
80    ///         Hash::from_str(
81    ///             "0000000000000000000000000000000000000000000000000000000000000000")
82    ///         .unwrap()),
83    ///     ("lib/mylib.so".to_string(),
84    ///         Hash::from_str(
85    ///             "1111111111111111111111111111111111111111111111111111111111111111")
86    ///         .unwrap()),
87    /// ]);
88    /// let meta_contents = MetaContents::from_map(map).unwrap();
89    /// let mut bytes = Vec::new();
90    /// meta_contents.serialize(&mut bytes).unwrap();
91    /// let expected = "bin/my_prog=0000000000000000000000000000000000000000000000000000000000000000\n\
92    ///                 lib/mylib.so=1111111111111111111111111111111111111111111111111111111111111111\n";
93    /// assert_eq!(bytes.as_slice(), expected.as_bytes());
94    /// ```
95    pub fn serialize(&self, writer: &mut impl io::Write) -> io::Result<()> {
96        let mut entries = self.contents.iter().collect::<Vec<_>>();
97        entries.sort();
98        for (path, hash) in entries {
99            writeln!(writer, "{path}={hash}")?;
100        }
101        Ok(())
102    }
103
104    /// Deserializes a "meta/contents" file from a `reader`.
105    ///
106    /// # Examples
107    /// ```
108    /// # use fuchsia_merkle::Hash;
109    /// # use fuchsia_pkg::MetaContents;
110    /// # use std::collections::HashMap;
111    /// # use std::str::FromStr;
112    /// let bytes = "bin/my_prog=0000000000000000000000000000000000000000000000000000000000000000\n\
113    ///              lib/mylib.so=1111111111111111111111111111111111111111111111111111111111111111\n".as_bytes();
114    /// let meta_contents = MetaContents::deserialize(bytes).unwrap();
115    /// let expected_contents = HashMap::from([
116    ///     ("bin/my_prog".to_string(),
117    ///         Hash::from_str(
118    ///             "0000000000000000000000000000000000000000000000000000000000000000")
119    ///         .unwrap()),
120    ///     ("lib/mylib.so".to_string(),
121    ///         Hash::from_str(
122    ///             "1111111111111111111111111111111111111111111111111111111111111111")
123    ///         .unwrap()),
124    /// ]);
125    /// assert_eq!(meta_contents.contents(), &expected_contents);
126    /// ```
127    pub fn deserialize(mut reader: impl io::BufRead) -> Result<Self, MetaContentsError> {
128        let mut contents = HashMap::new();
129        let mut buf = String::new();
130        while reader.read_line(&mut buf)? > 0 {
131            let line = buf.trim_end();
132            let i = line.rfind('=').ok_or_else(|| MetaContentsError::EntryHasNoEqualsSign {
133                entry: line.to_string(),
134            })?;
135
136            let hash = Hash::from_str(&line[i + 1..])?;
137            let path = line[..i].to_string();
138
139            use std::collections::hash_map::Entry;
140            match contents.entry(path) {
141                Entry::Vacant(entry) => {
142                    entry.insert(hash);
143                }
144                Entry::Occupied(entry) => {
145                    return Err(MetaContentsError::DuplicateResourcePath {
146                        path: entry.key().clone(),
147                    });
148                }
149            }
150
151            buf.clear();
152        }
153        contents.shrink_to_fit();
154        Self::from_map(contents)
155    }
156
157    /// Get the map from blob resource paths to Merkle Tree root hashes.
158    pub fn contents(&self) -> &HashMap<String, Hash> {
159        &self.contents
160    }
161
162    /// Take the map from blob resource paths to Merkle Tree root hashes.
163    pub fn into_contents(self) -> HashMap<String, Hash> {
164        self.contents
165    }
166
167    /// Take the Merkle Tree root hashes in a iterator. The returned iterator may include
168    /// duplicates.
169    pub fn into_hashes_undeduplicated(self) -> impl Iterator<Item = Hash> {
170        self.contents.into_values()
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use crate::test::*;
178    use assert_matches::assert_matches;
179    use fuchsia_url::ResourcePathError;
180    use fuchsia_url::test::*;
181    use proptest::prelude::*;
182
183    fn zeros_hash() -> Hash {
184        Hash::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap()
185    }
186
187    fn ones_hash() -> Hash {
188        Hash::from_str("1111111111111111111111111111111111111111111111111111111111111111").unwrap()
189    }
190
191    #[test]
192    fn deserialize_empty_file() {
193        let empty = Vec::new();
194        let meta_contents = MetaContents::deserialize(empty.as_slice()).unwrap();
195        assert_eq!(meta_contents.contents(), &HashMap::new());
196        assert_eq!(meta_contents.into_contents(), HashMap::new());
197    }
198
199    #[test]
200    fn deserialize_known_file() {
201        let bytes =
202            "a-host/path=0000000000000000000000000000000000000000000000000000000000000000\n\
203             other/host/path=1111111111111111111111111111111111111111111111111111111111111111\n"
204                .as_bytes();
205        let meta_contents = MetaContents::deserialize(bytes).unwrap();
206        let expected_contents = HashMap::from([
207            ("a-host/path".to_string(), zeros_hash()),
208            ("other/host/path".to_string(), ones_hash()),
209        ]);
210        assert_eq!(meta_contents.contents(), &expected_contents);
211        assert_eq!(meta_contents.into_contents(), expected_contents);
212    }
213
214    #[test]
215    fn from_map_rejects_meta_file() {
216        let map = HashMap::from([("meta".to_string(), zeros_hash())]);
217        assert_matches!(
218            MetaContents::from_map(map),
219            Err(MetaContentsError::ExternalContentInMetaDirectory { path }) if path == "meta"
220        );
221    }
222
223    #[test]
224    fn from_map_rejects_file_dir_collisions() {
225        for (map, expected_path) in [
226            (
227                HashMap::from([
228                    ("foo".to_string(), zeros_hash()),
229                    ("foo/bar".to_string(), zeros_hash()),
230                ]),
231                "foo",
232            ),
233            (
234                HashMap::from([
235                    ("foo/bar".to_string(), zeros_hash()),
236                    ("foo/bar/baz".to_string(), zeros_hash()),
237                ]),
238                "foo/bar",
239            ),
240            (
241                HashMap::from([
242                    ("foo".to_string(), zeros_hash()),
243                    ("foo/bar/baz".to_string(), zeros_hash()),
244                ]),
245                "foo",
246            ),
247        ] {
248            assert_matches!(
249                MetaContents::from_map(map),
250                Err(MetaContentsError::FileDirectoryCollision { path }) if path == expected_path
251            );
252        }
253    }
254
255    proptest! {
256        #![proptest_config(ProptestConfig{
257            failure_persistence: None,
258            ..Default::default()
259        })]
260
261        #[test]
262        fn from_map_rejects_invalid_resource_path(
263            ref path in random_resource_path(1, 3),
264            ref hex in random_merkle_hex())
265        {
266            prop_assume!(!path.starts_with("meta/"));
267            let invalid_path = format!("{path}/");
268            let map = HashMap::from([
269                (invalid_path.clone(), Hash::from_str(hex.as_str()).unwrap()),
270            ]);
271            assert_matches!(
272                MetaContents::from_map(map),
273                Err(MetaContentsError::InvalidResourcePath {
274                    cause: ResourcePathError::PathEndsWithSlash,
275                    path }) if path == invalid_path
276            );
277        }
278
279        #[test]
280        fn from_map_rejects_file_in_meta(
281            ref path in random_resource_path(1, 3),
282            ref hex in random_merkle_hex())
283        {
284            let invalid_path = format!("meta/{path}");
285            let map = HashMap::from([
286                (invalid_path.clone(), Hash::from_str(hex.as_str()).unwrap()),
287            ]);
288            assert_matches!(
289                MetaContents::from_map(map),
290                Err(MetaContentsError::ExternalContentInMetaDirectory { path }) if path == invalid_path
291            );
292        }
293
294        #[test]
295        fn serialize(
296            ref path0 in random_external_resource_path(),
297            ref hex0 in random_merkle_hex(),
298            ref path1 in random_external_resource_path(),
299            ref hex1 in random_merkle_hex())
300        {
301            prop_assume!(path0 != path1);
302            let map = HashMap::from([
303                (path0.clone(), Hash::from_str(hex0.as_str()).unwrap()),
304                (path1.clone(), Hash::from_str(hex1.as_str()).unwrap()),
305            ]);
306            let meta_contents = MetaContents::from_map(map);
307            prop_assume!(meta_contents.is_ok());
308            let meta_contents = meta_contents.unwrap();
309            let mut bytes = Vec::new();
310
311            meta_contents.serialize(&mut bytes).unwrap();
312
313            let ((first_path, first_hex), (second_path, second_hex)) = if path0 <= path1 {
314                ((path0, hex0), (path1, hex1))
315            } else {
316                ((path1, hex1), (path0, hex0))
317            };
318            let expected = format!(
319                "{}={}\n{}={}\n",
320                first_path,
321                first_hex.to_ascii_lowercase(),
322                second_path,
323                second_hex.to_ascii_lowercase());
324            prop_assert_eq!(bytes.as_slice(), expected.as_bytes());
325        }
326
327        #[test]
328        fn serialize_deserialize_is_id(
329            contents in prop::collection::hash_map(
330                random_external_resource_path(), random_hash(), 0..4)
331        ) {
332            let meta_contents = MetaContents::from_map(contents);
333            prop_assume!(meta_contents.is_ok());
334            let meta_contents = meta_contents.unwrap();
335            let mut serialized = Vec::new();
336            meta_contents.serialize(&mut serialized).unwrap();
337            let deserialized = MetaContents::deserialize(serialized.as_slice()).unwrap();
338            prop_assert_eq!(meta_contents, deserialized);
339        }
340    }
341}