1use crate::errors::PackageBuildManifestError;
6use serde::{Deserialize, Serialize};
7use std::collections::{BTreeMap, HashSet, btree_map};
8use std::fs;
9use std::io::{self, Read};
10use std::path::Path;
11use walkdir::WalkDir;
12
13#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
20#[serde(transparent)]
21pub struct PackageBuildManifest(VersionedPackageBuildManifest);
22
23impl PackageBuildManifest {
24 pub fn from_external_and_far_contents(
53 external_contents: BTreeMap<String, String>,
54 far_contents: BTreeMap<String, String>,
55 ) -> Result<Self, PackageBuildManifestError> {
56 for (resource_path, _) in external_contents.iter().chain(far_contents.iter()) {
57 fuchsia_url::Resource::validate_str(resource_path).map_err(|e| {
58 PackageBuildManifestError::ResourcePath {
59 cause: e,
60 path: resource_path.to_string(),
61 }
62 })?;
63 }
64 let external_paths =
65 external_contents.keys().map(|path| path.as_str()).collect::<HashSet<_>>();
66 for resource_path in &external_paths {
67 if resource_path.starts_with("meta/") || resource_path.eq(&"meta") {
68 return Err(PackageBuildManifestError::ExternalContentInMetaDirectory {
69 path: resource_path.to_string(),
70 });
71 }
72 for (i, _) in resource_path.match_indices('/') {
73 if external_paths.contains(&resource_path[..i]) {
74 return Err(PackageBuildManifestError::FileDirectoryCollision {
75 path: resource_path[..i].to_string(),
76 });
77 }
78 }
79 }
80 for (resource_path, _) in far_contents.iter() {
81 if !resource_path.starts_with("meta/") {
82 return Err(PackageBuildManifestError::FarContentNotInMetaDirectory {
83 path: resource_path.to_string(),
84 });
85 }
86 }
87 Ok(PackageBuildManifest(VersionedPackageBuildManifest::Version1(PackageBuildManifestV1 {
88 external_contents,
89 far_contents,
90 })))
91 }
92
93 pub fn from_json<R: io::Read>(reader: R) -> Result<Self, PackageBuildManifestError> {
112 match serde_json::from_reader::<R, VersionedPackageBuildManifest>(reader)? {
113 VersionedPackageBuildManifest::Version1(v1) => PackageBuildManifest::from_v1(v1),
114 }
115 }
116
117 fn from_v1(v1: PackageBuildManifestV1) -> Result<Self, PackageBuildManifestError> {
118 let mut far_contents = BTreeMap::new();
119 for (resource_path, host_path) in v1.far_contents.into_iter() {
122 fuchsia_url::Resource::validate_str(&resource_path).map_err(|e| {
123 PackageBuildManifestError::ResourcePath {
124 cause: e,
125 path: resource_path.to_string(),
126 }
127 })?;
128 far_contents.insert(format!("meta/{resource_path}"), host_path);
129 }
130 PackageBuildManifest::from_external_and_far_contents(v1.external_contents, far_contents)
131 }
132
133 pub fn from_dir(root: impl AsRef<Path>) -> Result<Self, PackageBuildManifestError> {
134 let root = root.as_ref();
135 let mut far_contents = BTreeMap::new();
136 let mut external_contents = BTreeMap::new();
137
138 for entry in WalkDir::new(root) {
139 let entry = entry?;
140 let path = entry.path();
141 let file_type = entry.file_type();
142 if file_type.is_dir() {
143 continue;
144 }
145 if !(file_type.is_file() || file_type.is_symlink()) {
146 return Err(PackageBuildManifestError::InvalidFileType {
147 path: path.to_path_buf(),
148 });
149 }
150
151 let relative_path = path
152 .strip_prefix(root)?
153 .to_str()
154 .ok_or(PackageBuildManifestError::EmptyResourcePath)?;
155 let path =
156 path.to_str().ok_or(PackageBuildManifestError::EmptyResourcePath)?.to_owned();
157 if relative_path.starts_with("meta") {
158 far_contents.insert(relative_path.to_owned(), path);
159 } else {
160 external_contents.insert(relative_path.to_owned(), path);
161 }
162 }
163
164 PackageBuildManifest::from_external_and_far_contents(external_contents, far_contents)
165 }
166
167 pub fn from_pm_fini<R: io::BufRead>(mut reader: R) -> Result<Self, PackageBuildManifestError> {
191 let mut external_contents = BTreeMap::new();
192 let mut far_contents = BTreeMap::new();
193
194 let mut buf = String::new();
195 while reader.read_line(&mut buf)? != 0 {
196 let line = buf.trim();
197 if line.is_empty() {
198 buf.clear();
199 continue;
200 }
201
202 let pos = if let Some(pos) = line.find('=') {
204 pos
205 } else {
206 buf.clear();
207 continue;
208 };
209
210 let package_path = line[..pos].trim().to_string();
211 let host_path = line[pos + 1..].trim().to_string();
212
213 let entry = if package_path.starts_with("meta/") {
214 far_contents.entry(package_path)
215 } else {
216 external_contents.entry(package_path)
217 };
218
219 match entry {
220 btree_map::Entry::Vacant(entry) => {
221 entry.insert(host_path);
222 }
223 btree_map::Entry::Occupied(entry) => {
224 if !same_file_contents(Path::new(&entry.get()), Path::new(&host_path))? {
227 return Err(PackageBuildManifestError::DuplicateResourcePath {
228 path: entry.key().clone(),
229 });
230 }
231 }
232 }
233
234 buf.clear();
235 }
236
237 Self::from_external_and_far_contents(external_contents, far_contents)
238 }
239
240 pub fn external_contents(&self) -> &BTreeMap<String, String> {
242 let VersionedPackageBuildManifest::Version1(manifest) = &self.0;
243 &manifest.external_contents
244 }
245
246 pub fn far_contents(&self) -> &BTreeMap<String, String> {
248 let VersionedPackageBuildManifest::Version1(manifest) = &self.0;
249 &manifest.far_contents
250 }
251}
252
253fn same_file_contents(lhs: &Path, rhs: &Path) -> io::Result<bool> {
256 if lhs == rhs {
258 return Ok(true);
259 }
260
261 #[cfg(unix)]
264 fn same_dev_inode(lhs: &Path, rhs: &Path) -> io::Result<bool> {
265 use std::os::unix::fs::MetadataExt;
266
267 let lhs = fs::metadata(lhs)?;
268 let rhs = fs::metadata(rhs)?;
269
270 Ok(lhs.dev() == rhs.dev() && lhs.ino() == rhs.ino())
271 }
272
273 #[cfg(not(unix))]
274 fn same_dev_inode(_lhs: &Path, _rhs: &Path) -> io::Result<bool> {
275 Ok(false)
276 }
277
278 if same_dev_inode(lhs, rhs)? {
279 return Ok(true);
280 }
281
282 let lhs = fs::canonicalize(lhs)?;
284 let rhs = fs::canonicalize(rhs)?;
285
286 if lhs == rhs {
287 return Ok(true);
288 }
289
290 let lhs = fs::File::open(lhs)?;
292 let rhs = fs::File::open(rhs)?;
293
294 if lhs.metadata()?.len() != rhs.metadata()?.len() {
295 return Ok(false);
296 }
297
298 let mut lhs = io::BufReader::new(lhs).bytes();
300 let mut rhs = io::BufReader::new(rhs).bytes();
301
302 loop {
303 match (lhs.next(), rhs.next()) {
304 (None, None) => {
305 return Ok(true);
306 }
307 (Some(Ok(_)), None) | (None, Some(Ok(_))) => {
308 return Ok(false);
309 }
310 (Some(Ok(lhs_byte)), Some(Ok(rhs_byte))) => {
311 if lhs_byte != rhs_byte {
312 return Ok(false);
313 }
314 }
315 (Some(Err(err)), _) | (_, Some(Err(err))) => {
316 return Err(err);
317 }
318 }
319 }
320}
321
322#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
323#[serde(tag = "version", content = "content", deny_unknown_fields)]
324enum VersionedPackageBuildManifest {
325 #[serde(rename = "1")]
326 Version1(PackageBuildManifestV1),
327}
328
329#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
330struct PackageBuildManifestV1 {
331 #[serde(rename = "/")]
332 external_contents: BTreeMap<String, String>,
333 #[serde(rename = "/meta/")]
334 far_contents: BTreeMap<String, String>,
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340 use crate::test::*;
341 use assert_matches::assert_matches;
342 use proptest::prelude::*;
343 use serde_json::json;
344 use std::fs::create_dir;
345
346 fn from_json_value(
347 value: serde_json::Value,
348 ) -> Result<PackageBuildManifest, PackageBuildManifestError> {
349 PackageBuildManifest::from_json(value.to_string().as_bytes())
350 }
351
352 #[test]
353 fn test_malformed_json() {
354 assert_matches!(
355 PackageBuildManifest::from_json("<invalid json document>".as_bytes()),
356 Err(PackageBuildManifestError::Json(err)) if err.is_syntax()
357 );
358 }
359
360 #[test]
361 fn test_invalid_version() {
362 assert_matches!(
363 from_json_value(json!({"version": "2", "content": {}})),
364 Err(PackageBuildManifestError::Json(err)) if err.is_data()
365 );
366 }
367
368 #[test]
369 fn test_invalid_resource_path() {
370 assert_matches!(
371 from_json_value(
372 json!(
373 {"version": "1",
374 "content":
375 {"/meta/" :
376 {"/starts-with-slash": "host-path"},
377 "/": {
378 }
379 }
380 }
381 )
382 ),
383 Err(PackageBuildManifestError::ResourcePath {
384 cause: fuchsia_url::ResourcePathError::PathStartsWithSlash,
385 path: s
386 }) if s == "/starts-with-slash"
387 );
388 }
389
390 #[test]
391 fn test_meta_dir_in_external() {
392 assert_matches!(
393 from_json_value(
394 json!(
395 {"version": "1",
396 "content":
397 {"/meta/" : {},
398 "/": {
399 "meta/foo": "host-path"}
400 }
401 }
402 )
403 ),
404 Err(PackageBuildManifestError::ExternalContentInMetaDirectory{path: s}) if s == "meta/foo"
405 );
406 }
407
408 #[test]
409 fn test_meta_file_in_external() {
410 assert_matches!(
411 from_json_value(
412 json!({
413 "version": "1",
414 "content": {
415 "/meta/" : {},
416 "/": {
417 "meta": "host-path"
418 }
419 }
420 })
421 ),
422 Err(PackageBuildManifestError::ExternalContentInMetaDirectory{path: s}) if s == "meta"
423 );
424 }
425
426 #[test]
427 fn test_file_dir_collision() {
428 for (path0, path1, expected_conflict) in [
429 ("foo", "foo/bar", "foo"),
430 ("foo/bar", "foo/bar/baz", "foo/bar"),
431 ("foo", "foo/bar/baz", "foo"),
432 ] {
433 let external = BTreeMap::from([
434 (path0.to_string(), String::new()),
435 (path1.to_string(), String::new()),
436 ]);
437 assert_matches!(
438 PackageBuildManifest::from_external_and_far_contents(external, BTreeMap::new()),
439 Err(PackageBuildManifestError::FileDirectoryCollision { path })
440 if path == expected_conflict
441 );
442 }
443 }
444
445 #[test]
446 fn test_from_v1() {
447 assert_eq!(
448 from_json_value(json!(
449 {"version": "1",
450 "content": {
451 "/": {
452 "this-path": "this-host-path",
453 "that/path": "that/host/path"},
454 "/meta/" : {
455 "some-path": "some-host-path",
456 "other/path": "other/host/path"}
457 }
458 }
459 ))
460 .unwrap(),
461 PackageBuildManifest(VersionedPackageBuildManifest::Version1(PackageBuildManifestV1 {
462 external_contents: BTreeMap::from([
463 ("this-path".to_string(), "this-host-path".to_string()),
464 ("that/path".to_string(), "that/host/path".to_string()),
465 ]),
466 far_contents: BTreeMap::from([
467 ("meta/some-path".to_string(), "some-host-path".to_string()),
468 ("meta/other/path".to_string(), "other/host/path".to_string()),
469 ])
470 }))
471 );
472 }
473
474 #[test]
475 fn test_from_pm_fini() {
476 assert_eq!(
477 PackageBuildManifest::from_pm_fini(
478 "this-path=this-host-path\n\
479 that/path=that/host/path\n\
480 another/path=another/host=path\n
481 with/white/space = host/white/space \n\n\
482 meta/some-path=some-host-path\n\
483 meta/other/path=other/host/path\n\
484 meta/another/path=another/host=path\n\
485 ignore lines without equals"
486 .as_bytes()
487 )
488 .unwrap(),
489 PackageBuildManifest(VersionedPackageBuildManifest::Version1(PackageBuildManifestV1 {
490 external_contents: BTreeMap::from([
491 ("this-path".to_string(), "this-host-path".to_string()),
492 ("that/path".to_string(), "that/host/path".to_string()),
493 ("another/path".to_string(), "another/host=path".to_string()),
494 ("with/white/space".to_string(), "host/white/space".to_string()),
495 ]),
496 far_contents: BTreeMap::from([
497 ("meta/some-path".to_string(), "some-host-path".to_string()),
498 ("meta/other/path".to_string(), "other/host/path".to_string()),
499 ("meta/another/path".to_string(), "another/host=path".to_string()),
500 ]),
501 })),
502 );
503 }
504
505 #[test]
506 fn test_from_pm_fini_empty() {
507 assert_eq!(
508 PackageBuildManifest::from_pm_fini("".as_bytes()).unwrap(),
509 PackageBuildManifest(VersionedPackageBuildManifest::Version1(PackageBuildManifestV1 {
510 external_contents: BTreeMap::new(),
511 far_contents: BTreeMap::new()
512 })),
513 );
514 }
515
516 #[test]
517 fn test_from_pm_fini_same_file_contents() {
518 let dir = tempfile::tempdir().unwrap();
519
520 let path = dir.path().join("path");
521 let same = dir.path().join("same");
522
523 fs::write(&path, b"hello world").unwrap();
524 fs::write(&same, b"hello world").unwrap();
525
526 let fini = format!(
527 "path={path}\n\
528 path={same}\n",
529 path = path.to_str().unwrap(),
530 same = same.to_str().unwrap(),
531 );
532
533 assert_eq!(
534 PackageBuildManifest::from_pm_fini(fini.as_bytes()).unwrap(),
535 PackageBuildManifest(VersionedPackageBuildManifest::Version1(PackageBuildManifestV1 {
536 external_contents: BTreeMap::from([(
537 "path".to_string(),
538 path.to_str().unwrap().to_string()
539 ),]),
540 far_contents: BTreeMap::new(),
541 })),
542 );
543 }
544
545 #[test]
546 fn test_from_pm_fini_different_contents() {
547 let dir = tempfile::tempdir().unwrap();
548
549 let path = dir.path().join("path");
550 let different = dir.path().join("different");
551
552 fs::write(&path, b"hello world").unwrap();
553 fs::write(&different, b"different").unwrap();
554
555 let fini = format!(
556 "path={path}\n\
557 path={different}\n",
558 path = path.to_str().unwrap(),
559 different = different.to_str().unwrap()
560 );
561
562 assert_matches!(
563 PackageBuildManifest::from_pm_fini(fini.as_bytes()),
564 Err(PackageBuildManifestError::DuplicateResourcePath { path }) if path == "path"
565 );
566 }
567
568 #[test]
569 fn test_from_dir() {
570 let dir = tempfile::tempdir().unwrap();
571
572 let blob1 = dir.path().join("blob1");
573 let blob2 = dir.path().join("blob2");
574 let meta_dir = dir.path().join("meta");
575 create_dir(&meta_dir).unwrap();
576
577 let meta_package = meta_dir.join("package");
578 let meta_data = meta_dir.join("data");
579
580 fs::write(blob1, b"blob1").unwrap();
581 fs::write(blob2, b"blob2").unwrap();
582 fs::write(meta_package, b"meta_package").unwrap();
583 fs::write(meta_data, b"meta_data").unwrap();
584
585 let creation_manifest = PackageBuildManifest::from_dir(dir.path()).unwrap();
586 let far_contents = creation_manifest.far_contents();
587 let external_contents = creation_manifest.external_contents();
588 assert!(far_contents.contains_key("meta/data"));
589 assert!(far_contents.contains_key("meta/package"));
590 assert!(external_contents.contains_key("blob1"));
591 assert!(external_contents.contains_key("blob2"));
592 }
593
594 #[test]
595 fn test_from_pm_fini_not_found() {
596 let dir = tempfile::tempdir().unwrap();
597
598 let path = dir.path().join("path");
599 let not_found = dir.path().join("not_found");
600
601 fs::write(&path, b"hello world").unwrap();
602
603 let fini = format!(
604 "path={path}\n\
605 path={not_found}\n",
606 path = path.to_str().unwrap(),
607 not_found = not_found.to_str().unwrap()
608 );
609
610 assert_matches!(
611 PackageBuildManifest::from_pm_fini(fini.as_bytes()),
612 Err(PackageBuildManifestError::IoError(err)) if err.kind() == io::ErrorKind::NotFound
613 );
614 }
615
616 #[cfg(not(target_os = "fuchsia"))]
617 #[cfg(unix)]
618 #[test]
619 fn test_from_pm_fini_link() {
620 let dir = tempfile::tempdir().unwrap();
621
622 let path = dir.path().join("path");
623 let hard = dir.path().join("hard");
624 let sym = dir.path().join("symlink");
625
626 fs::write(&path, b"hello world").unwrap();
627 fs::hard_link(&path, &hard).unwrap();
628 std::os::unix::fs::symlink(&path, &sym).unwrap();
629
630 let fini = format!(
631 "path={path}\n\
632 path={hard}\n\
633 path={sym}\n",
634 path = path.to_str().unwrap(),
635 hard = hard.to_str().unwrap(),
636 sym = sym.to_str().unwrap(),
637 );
638
639 assert_eq!(
640 PackageBuildManifest::from_pm_fini(fini.as_bytes()).unwrap(),
641 PackageBuildManifest(VersionedPackageBuildManifest::Version1(PackageBuildManifestV1 {
642 external_contents: BTreeMap::from([(
643 "path".to_string(),
644 path.to_str().unwrap().to_string()
645 ),]),
646 far_contents: BTreeMap::new(),
647 })),
648 );
649 }
650
651 proptest! {
652 #[test]
653 fn test_from_external_and_far_contents_does_not_modify_valid_maps(
654 ref external_resource_path in random_external_resource_path(),
655 ref external_host_path in ".{0,30}",
656 ref far_resource_path in random_far_resource_path(),
657 ref far_host_path in ".{0,30}"
658 ) {
659 let external_contents = BTreeMap::from([
660 (external_resource_path.to_string(), external_host_path.to_string()),
661 ]);
662 let far_resource_path = format!("meta/{far_resource_path}");
663 let far_contents = BTreeMap::from([
664 (far_resource_path, far_host_path.to_string()),
665 ]);
666
667 let creation_manifest = PackageBuildManifest::from_external_and_far_contents(
668 external_contents.clone(), far_contents.clone())
669 .unwrap();
670
671 prop_assert_eq!(creation_manifest.external_contents(), &external_contents);
672 prop_assert_eq!(creation_manifest.far_contents(), &far_contents);
673 }
674 }
675}