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