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 maplit::hashmap;
37    /// # use std::str::FromStr;
38    /// let map = hashmap! {
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 maplit::hashmap;
77    /// # use std::str::FromStr;
78    /// let map = hashmap! {
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 maplit::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! {
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 maplit::hashmap;
182    use proptest::prelude::*;
183
184    fn zeros_hash() -> Hash {
185        Hash::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap()
186    }
187
188    fn ones_hash() -> Hash {
189        Hash::from_str("1111111111111111111111111111111111111111111111111111111111111111").unwrap()
190    }
191
192    #[test]
193    fn deserialize_empty_file() {
194        let empty = Vec::new();
195        let meta_contents = MetaContents::deserialize(empty.as_slice()).unwrap();
196        assert_eq!(meta_contents.contents(), &HashMap::new());
197        assert_eq!(meta_contents.into_contents(), HashMap::new());
198    }
199
200    #[test]
201    fn deserialize_known_file() {
202        let bytes =
203            "a-host/path=0000000000000000000000000000000000000000000000000000000000000000\n\
204             other/host/path=1111111111111111111111111111111111111111111111111111111111111111\n"
205                .as_bytes();
206        let meta_contents = MetaContents::deserialize(bytes).unwrap();
207        let expected_contents = hashmap! {
208            "a-host/path".to_string() => zeros_hash(),
209            "other/host/path".to_string() => ones_hash(),
210        };
211        assert_eq!(meta_contents.contents(), &expected_contents);
212        assert_eq!(meta_contents.into_contents(), expected_contents);
213    }
214
215    #[test]
216    fn from_map_rejects_meta_file() {
217        let map = hashmap! {
218            "meta".to_string() => zeros_hash(),
219        };
220        assert_matches!(
221            MetaContents::from_map(map),
222            Err(MetaContentsError::ExternalContentInMetaDirectory { path }) if path == "meta"
223        );
224    }
225
226    #[test]
227    fn from_map_rejects_file_dir_collisions() {
228        for (map, expected_path) in [
229            (
230                hashmap! {
231                    "foo".to_string() => zeros_hash(),
232                    "foo/bar".to_string() => zeros_hash(),
233                },
234                "foo",
235            ),
236            (
237                hashmap! {
238                    "foo/bar".to_string() => zeros_hash(),
239                    "foo/bar/baz".to_string() => zeros_hash(),
240                },
241                "foo/bar",
242            ),
243            (
244                hashmap! {
245                    "foo".to_string() => zeros_hash(),
246                    "foo/bar/baz".to_string() => zeros_hash(),
247                },
248                "foo",
249            ),
250        ] {
251            assert_matches!(
252                MetaContents::from_map(map),
253                Err(MetaContentsError::FileDirectoryCollision { path }) if path == expected_path
254            );
255        }
256    }
257
258    proptest! {
259        #![proptest_config(ProptestConfig{
260            failure_persistence: None,
261            ..Default::default()
262        })]
263
264        #[test]
265        fn from_map_rejects_invalid_resource_path(
266            ref path in random_resource_path(1, 3),
267            ref hex in random_merkle_hex())
268        {
269            prop_assume!(!path.starts_with("meta/"));
270            let invalid_path = format!("{path}/");
271            let map = hashmap! {
272                invalid_path.clone() =>
273                    Hash::from_str(hex.as_str()).unwrap(),
274            };
275            assert_matches!(
276                MetaContents::from_map(map),
277                Err(MetaContentsError::InvalidResourcePath {
278                    cause: ResourcePathError::PathEndsWithSlash,
279                    path }) if path == invalid_path
280            );
281        }
282
283        #[test]
284        fn from_map_rejects_file_in_meta(
285            ref path in random_resource_path(1, 3),
286            ref hex in random_merkle_hex())
287        {
288            let invalid_path = format!("meta/{path}");
289            let map = hashmap! {
290                invalid_path.clone() =>
291                    Hash::from_str(hex.as_str()).unwrap(),
292            };
293            assert_matches!(
294                MetaContents::from_map(map),
295                Err(MetaContentsError::ExternalContentInMetaDirectory { path }) if path == invalid_path
296            );
297        }
298
299        #[test]
300        fn serialize(
301            ref path0 in random_external_resource_path(),
302            ref hex0 in random_merkle_hex(),
303            ref path1 in random_external_resource_path(),
304            ref hex1 in random_merkle_hex())
305        {
306            prop_assume!(path0 != path1);
307            let map = hashmap! {
308                path0.clone() =>
309                    Hash::from_str(hex0.as_str()).unwrap(),
310                path1.clone() =>
311                    Hash::from_str(hex1.as_str()).unwrap(),
312            };
313            let meta_contents = MetaContents::from_map(map);
314            prop_assume!(meta_contents.is_ok());
315            let meta_contents = meta_contents.unwrap();
316            let mut bytes = Vec::new();
317
318            meta_contents.serialize(&mut bytes).unwrap();
319
320            let ((first_path, first_hex), (second_path, second_hex)) = if path0 <= path1 {
321                ((path0, hex0), (path1, hex1))
322            } else {
323                ((path1, hex1), (path0, hex0))
324            };
325            let expected = format!(
326                "{}={}\n{}={}\n",
327                first_path,
328                first_hex.to_ascii_lowercase(),
329                second_path,
330                second_hex.to_ascii_lowercase());
331            prop_assert_eq!(bytes.as_slice(), expected.as_bytes());
332        }
333
334        #[test]
335        fn serialize_deserialize_is_id(
336            contents in prop::collection::hash_map(
337                random_external_resource_path(), random_hash(), 0..4)
338        ) {
339            let meta_contents = MetaContents::from_map(contents);
340            prop_assume!(meta_contents.is_ok());
341            let meta_contents = meta_contents.unwrap();
342            let mut serialized = Vec::new();
343            meta_contents.serialize(&mut serialized).unwrap();
344            let deserialized = MetaContents::deserialize(serialized.as_slice()).unwrap();
345            prop_assert_eq!(meta_contents, deserialized);
346        }
347    }
348}