1use crate::errors::MetaContentsError;
6use buf_read_ext::BufReadExt;
7use fuchsia_merkle::Hash;
8use packed::PackedMap;
9use std::io;
10use std::str::FromStr;
11
12#[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 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 pub fn new() -> Self {
73 Self { contents: PackedMap::new() }
74 }
75
76 pub fn get(&self, path: &str) -> Option<&Hash> {
78 self.contents.get(path)
79 }
80
81 pub fn keys(&self) -> impl Iterator<Item = &str> {
83 self.contents.keys()
84 }
85
86 pub fn values(&self) -> impl Iterator<Item = &Hash> {
88 self.contents.values()
89 }
90
91 pub fn iter(&self) -> packed::Iter<'_, str, Hash> {
93 self.contents.iter()
94 }
95
96 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 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 pub fn contents(&self) -> &PackedMap<str, Hash> {
173 &self.contents
174 }
175
176 pub fn into_contents(self) -> PackedMap<str, Hash> {
178 self.contents
179 }
180
181 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}