fuchsia_pkg/
meta_contents.rs1use crate::errors::MetaContentsError;
6use fuchsia_merkle::Hash;
7use std::collections::HashMap;
8use std::io;
9use std::str::FromStr;
10
11#[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 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 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 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 pub fn contents(&self) -> &HashMap<String, Hash> {
159 &self.contents
160 }
161
162 pub fn into_contents(self) -> HashMap<String, Hash> {
164 self.contents
165 }
166
167 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}