1use anyhow::{Context as _, Error, format_err};
8use blobfs_ramdisk::BlobfsRamdisk;
9use camino::{Utf8Path, Utf8PathBuf};
10use fidl_fuchsia_io as fio;
11use fuchsia_merkle::Hash;
12use fuchsia_pkg::{MetaContents, MetaSubpackages, PackageManifest};
13use fuchsia_url::{PackageName, PinnedAbsolutePackageUrl};
14use futures::join;
15use futures::prelude::*;
16use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
17use std::convert::TryInto as _;
18use std::fs::{self, File};
19use std::io::{self, Read};
20use std::path::{Path, PathBuf};
21use tempfile::TempDir;
22use version_history::AbiRevision;
23use walkdir::WalkDir;
24use zx::Status;
25
26#[derive(Debug)]
28pub struct Package {
29 name: PackageName,
30 meta_far_merkle: Hash,
31 _artifacts_tmp: TempDir,
32 artifacts: Utf8PathBuf,
33 subpackage_blobs: Option<HashMap<Hash, Vec<u8>>>,
36}
37
38#[derive(Debug, PartialEq)]
39enum PackageEntry {
40 Directory,
41 File(Vec<u8>),
42}
43
44impl PackageEntry {
45 fn is_dir(&self) -> bool {
46 matches!(self, PackageEntry::Directory)
47 }
48}
49pub struct BlobFile {
50 pub merkle: fuchsia_merkle::Hash,
51 pub file: File,
52}
53
54pub struct BlobContents {
56 pub merkle: fuchsia_merkle::Hash,
58
59 pub contents: Vec<u8>,
61}
62
63impl Package {
64 pub fn hash(&self) -> &Hash {
66 &self.meta_far_merkle
67 }
68
69 pub fn meta_far(&self) -> io::Result<File> {
71 File::open(self.artifacts.join("meta.far"))
72 }
73
74 pub fn name(&self) -> &PackageName {
76 &self.name
77 }
78
79 pub fn fuchsia_url(&self) -> PinnedAbsolutePackageUrl {
81 let unpinned = format!("fuchsia-pkg://fuchsia.com/{}", self.name).parse().unwrap();
82 PinnedAbsolutePackageUrl::from_unpinned(unpinned, self.meta_far_merkle)
83 }
84
85 pub fn artifacts(&self) -> &Utf8Path {
87 &self.artifacts
88 }
89
90 pub async fn identity() -> Result<Self, Error> {
92 Self::from_dir("/pkg").await
93 }
94
95 pub async fn from_dir(root: impl AsRef<Path>) -> Result<Self, Error> {
97 let root = root.as_ref();
98 let package_directory = fuchsia_pkg::PackageDirectory::from_proxy(
99 fuchsia_fs::directory::open_in_namespace(root.to_str().unwrap(), fio::PERM_READABLE)?,
100 );
101
102 let meta_package = package_directory.meta_package().await.context("read meta/package")?;
103 let abi_revision = package_directory.abi_revision().await.context("read abi revision")?;
104
105 let mut pkg =
106 PackageBuilder::new_with_abi_revision(meta_package.name().as_ref(), abi_revision);
107
108 fn is_generated_file(path: &Path) -> bool {
109 matches!(
110 path.to_str(),
111 Some("meta/contents")
112 | Some("meta/package")
113 | Some(AbiRevision::PATH)
114 | Some(MetaSubpackages::PATH)
115 )
116 }
117
118 for entry in WalkDir::new(root) {
120 let entry = entry?;
121 let path = entry.path();
122 if !entry.file_type().is_file() || is_generated_file(path.strip_prefix(root).unwrap()) {
123 continue;
124 }
125
126 let relative_path = path.strip_prefix(root).unwrap();
127 let f = File::open(path).context("open package blob")?;
128 pkg = pkg.add_resource_at(relative_path.to_str().unwrap(), f);
129 }
130
131 let subpackages = package_directory
132 .meta_subpackages()
133 .await
134 .context("read meta subpackages")?
135 .into_subpackages();
136 if !subpackages.is_empty() {
137 for (name, hash) in subpackages.into_iter() {
138 pkg = pkg.add_subpackage_by_hash(name, hash);
139 }
140 }
141
142 pkg.build().await
143 }
144
145 pub fn meta_contents(&self) -> Result<MetaContents, Error> {
147 let mut raw_meta_far = self.meta_far()?;
148 let mut meta_far = fuchsia_archive::Utf8Reader::new(&mut raw_meta_far)?;
149 let raw_meta_contents = meta_far.read_file("meta/contents")?;
150
151 Ok(MetaContents::deserialize(raw_meta_contents.as_slice())?)
152 }
153
154 pub fn meta_subpackages(&self) -> Result<MetaSubpackages, Error> {
156 let mut raw_meta_far = self.meta_far()?;
157 let mut meta_far = fuchsia_archive::Utf8Reader::new(&mut raw_meta_far)?;
158 Ok(match meta_far.read_file(MetaSubpackages::PATH) {
159 Ok(bytes) => MetaSubpackages::deserialize(std::io::BufReader::new(bytes.as_slice()))?,
160 Err(fuchsia_archive::Error::PathNotPresent(_)) => MetaSubpackages::default(),
161 Err(e) => Err(e)?,
162 })
163 }
164
165 pub fn list_blobs(&self) -> BTreeSet<Hash> {
171 self.meta_contents()
172 .expect("loading meta/contents")
173 .into_hashes_undeduplicated()
174 .chain([self.meta_far_merkle])
175 .chain(
176 self.subpackage_blobs
177 .as_ref()
178 .unwrap_or_else(|| {
179 panic!(
180 "cannot list blobs for package {} with unknown subpackage blobs",
181 self.name()
182 )
183 })
184 .keys()
185 .copied(),
186 )
187 .collect()
188 }
189
190 pub fn content_blob_files(&self) -> impl Iterator<Item = BlobFile> {
194 let manifest =
195 fuchsia_pkg::PackageManifest::try_load_from(self.artifacts().join("manifest.json"))
196 .unwrap();
197 struct Blob {
198 merkle: fuchsia_merkle::Hash,
199 path: Utf8PathBuf,
200 }
201 #[allow(clippy::needless_collect)]
202 let blobs = manifest
203 .into_blobs()
204 .into_iter()
205 .filter(|blob| blob.path != PackageManifest::META_FAR_BLOB_PATH)
206 .map(|blob| Blob {
207 merkle: blob.merkle,
208 path: self.artifacts().join(&blob.source_path),
209 })
210 .collect::<Vec<_>>();
211
212 blobs
213 .into_iter()
214 .map(|blob| BlobFile { merkle: blob.merkle, file: File::open(blob.path).unwrap() })
215 }
216
217 pub fn contents(&self) -> (BlobContents, HashMap<Hash, Vec<u8>>) {
219 (
220 BlobContents {
221 merkle: self.meta_far_merkle,
222 contents: io::BufReader::new(self.meta_far().unwrap())
223 .bytes()
224 .collect::<Result<Vec<u8>, _>>()
225 .unwrap(),
226 },
227 self.content_blob_files()
228 .map(|blob_file| {
229 (
230 blob_file.merkle,
231 io::BufReader::new(blob_file.file)
232 .bytes()
233 .collect::<Result<Vec<u8>, _>>()
234 .unwrap(),
235 )
236 })
237 .collect(),
238 )
239 }
240
241 pub fn content_and_subpackage_blobs(&self) -> Option<HashMap<Hash, Vec<u8>>> {
243 if let Some(subpackage_blobs) = &self.subpackage_blobs {
244 let mut subpackage_blobs = subpackage_blobs.clone();
245 subpackage_blobs.extend(self.content_blob_files().map(|blob_file| {
246 (
247 blob_file.merkle,
248 io::BufReader::new(blob_file.file)
249 .bytes()
250 .collect::<Result<Vec<u8>, _>>()
251 .unwrap(),
252 )
253 }));
254 Some(subpackage_blobs)
255 } else {
256 None
257 }
258 }
259
260 pub async fn write_to_blobfs_ignore_subpackages(&self, blobfs_ramdisk: &BlobfsRamdisk) {
263 fn read_file(file: &std::fs::File) -> Vec<u8> {
264 let mut ret = vec![];
265 std::io::BufReader::new(file).read_to_end(&mut ret).unwrap();
266 ret
267 }
268
269 blobfs_ramdisk
270 .write_blob(*self.hash(), &read_file(&self.meta_far().unwrap()))
271 .await
272 .expect("write_blob failed");
273 for blob in self.content_blob_files() {
274 blobfs_ramdisk
275 .write_blob(blob.merkle, &read_file(&blob.file))
276 .await
277 .expect("write_blob failed");
278 }
279 }
280
281 pub async fn write_to_blobfs(&self, blobfs_ramdisk: &BlobfsRamdisk) {
283 let subpackage_blobs = self
284 .subpackage_blobs
285 .as_ref()
286 .expect("package must know the subpackage blobs to write them");
287 let () = self.write_to_blobfs_ignore_subpackages(blobfs_ramdisk).await;
288 for (hash, content) in subpackage_blobs {
289 blobfs_ramdisk.write_blob(*hash, content).await.expect("write_blob failed");
290 }
291 }
292
293 pub async fn verify_contents(
295 &self,
296 dir: &fio::DirectoryProxy,
297 ) -> Result<(), VerificationError> {
298 let mut raw_meta_far = self.meta_far()?;
299 let mut meta_far = fuchsia_archive::Utf8Reader::new(&mut raw_meta_far)?;
300 let mut expected_paths = HashSet::new();
301
302 let raw_meta_contents = meta_far.read_file("meta/contents")?;
304 let meta_contents = MetaContents::deserialize(raw_meta_contents.as_slice())?;
305 for (path, merkle) in meta_contents.contents() {
306 let actual_merkle = fuchsia_merkle::root_from_slice(read_file(dir, path).await?);
307 if merkle != &actual_merkle {
308 return Err(VerificationError::DifferentFileData { path: path.to_owned() });
309 }
310 expected_paths.insert(path.clone());
311 }
312
313 for path in meta_far.list().map(|e| e.path().to_string()).collect::<Vec<_>>() {
315 if read_file(dir, path.as_str()).await? != meta_far.read_file(path.as_str())? {
316 return Err(VerificationError::DifferentFileData { path });
317 }
318 expected_paths.insert(path);
319 }
320
321 let mut stream = fuchsia_fs::directory::readdir_recursive(dir, None);
323 while let Some(entry) = stream.try_next().await? {
324 let path = entry.name;
325 if !expected_paths.contains(path.as_str()) {
326 return Err(VerificationError::ExtraFile { path });
327 }
328 }
329
330 Ok(())
331 }
332
333 pub fn subpackage_blobs(&self) -> Option<&HashMap<Hash, Vec<u8>>> {
337 self.subpackage_blobs.as_ref()
338 }
339}
340
341async fn read_file(dir: &fio::DirectoryProxy, path: &str) -> Result<Vec<u8>, VerificationError> {
342 let (file, server_end) = fidl::endpoints::create_proxy::<fio::FileMarker>();
343
344 let flags = fio::Flags::FLAG_SEND_REPRESENTATION | fio::PERM_READABLE;
345 dir.open(path, flags, &fio::Options::default(), server_end.into_channel())
346 .expect("open3 request failed to send");
347
348 let mut events = file.take_event_stream();
349 let open = async move {
350 let event = match events.next().await.expect("Some(event)") {
351 Ok(representation) => match representation {
352 fio::FileEvent::OnOpen_ { s, info } => {
353 match Status::ok(s) {
354 Err(Status::NOT_FOUND) => {
355 Err(VerificationError::MissingFile { path: path.to_owned() })
356 }
357 Err(status) => {
358 Err(format_err!("unable to open {:?}: {:?}", path, status).into())
359 }
360 Ok(()) => Ok(()),
361 }?;
362
363 match *info.expect("fio::FileEvent to have fio::NodeInfoDeprecated") {
364 fio::NodeInfoDeprecated::File(fio::FileObject { event, .. }) => event,
365 other => {
366 panic!(
367 "fio::NodeInfoDeprecated from fio::FileEventStream to be File variant with event: {other:?}"
368 )
369 }
370 }
371 }
372 fio::FileEvent::OnRepresentation { payload } => match payload {
373 fio::Representation::File(fio::FileInfo { observer, .. }) => observer,
374 other => {
375 panic!(
376 "ConnectionInfo from fio::FileEventStream to be File variant with event: {other:?}"
377 )
378 }
379 },
380 fio::FileEvent::_UnknownEvent { ordinal, .. } => {
381 panic!("unknown file event {ordinal}")
382 }
383 },
384 Err(fidl::Error::ClientChannelClosed { status, .. })
386 if status == zx::Status::NOT_FOUND =>
387 {
388 return Err(VerificationError::MissingFile { path: path.to_owned() });
389 }
390 Err(other_e) => {
391 return Err(
392 format_err!("open fidl request at {:?} failed: {:?}", path, other_e).into()
393 );
394 }
395 };
396
397 if let Some(event) = event {
408 match event
409 .wait_one(
410 zx::Signals::USER_0,
411 zx::MonotonicInstant::after(zx::MonotonicDuration::from_seconds(0)),
412 )
413 .to_result()
414 {
415 Err(Status::TIMED_OUT) => Err(VerificationError::from(format_err!(
416 "file served by blobfs is not complete/readable as USER_0 signal was not set on the File's event: {}",
417 path
418 ))),
419 Err(other_status) => Err(VerificationError::from(format_err!(
420 "wait_handle failed with status: {:?} {:?}",
421 other_status,
422 path
423 ))),
424 Ok(_) => Ok(()),
425 }
426 } else {
427 Ok(())
428 }
429 };
430
431 let read = async {
432 let result = file.get_backing_memory(fio::VmoFlags::READ).await?.map_err(Status::from_raw);
433
434 let mut expect_empty_blob = false;
435
436 match result {
438 Ok(vmo) => {
439 let size = vmo.get_content_size().context("unable to get vmo size")?;
440 let mut buf = vec![0u8; size as usize];
441 let () = vmo.read(&mut buf[..], 0).context("unable to read from vmo")?;
442 return Ok(buf);
443 }
444 Err(status) => match status {
445 Status::NOT_SUPPORTED => {}
446 Status::BAD_STATE => {
447 expect_empty_blob = true;
451 }
452 status => {
453 return Err(VerificationError::from(format_err!(
454 "unexpected error opening file buffer: {:?}",
455 status
456 )));
457 }
458 },
459 }
460
461 let mut buf = vec![];
462 loop {
463 let chunk = file
464 .read(fio::MAX_BUF)
465 .await
466 .context("file read to respond")?
467 .map_err(Status::from_raw)
468 .map_err(|status| VerificationError::FileReadError { path: path.into(), status })?;
469
470 if chunk.is_empty() {
471 if expect_empty_blob {
472 assert_eq!(buf, Vec::<u8>::new());
473 }
474 return Ok(buf);
475 }
476
477 buf.extend(chunk);
478 }
479 };
480
481 let (open, read) = join!(open, read);
482 let close_result = file.close().await;
483 let result = open.and(read)?;
484 let close_result = close_result.context("file close to respond")?;
486 close_result.map_err(|status| {
487 format_err!("unable to close {:?}: {:?}", path, zx::Status::from_raw(status))
488 })?;
489 Ok(result)
490}
491
492#[derive(Debug)]
494pub enum VerificationError {
495 ExtraFile {
497 path: String,
499 },
500 MissingFile {
502 path: String,
504 },
505 DifferentFileData {
507 path: String,
509 },
510 FileReadError {
512 path: String,
514 status: Status,
516 },
517 Other(Error),
519}
520
521impl<T: Into<Error>> From<T> for VerificationError {
522 fn from(x: T) -> Self {
523 VerificationError::Other(x.into())
524 }
525}
526
527pub struct PackageBuilder {
529 name: PackageName,
530 contents: BTreeMap<PathBuf, PackageEntry>,
531
532 has_subpackages: bool,
533 subpackage_blobs: Option<HashMap<Hash, Vec<u8>>>,
536
537 builder: fuchsia_pkg::PackageBuilder,
538 _artifacts_tmp: TempDir,
539 artifacts: Utf8PathBuf,
540}
541
542impl PackageBuilder {
543 pub fn new(name: impl Into<String>) -> Self {
551 Self::new_with_abi_revision(
552 name,
553 0xECCEA2F70ACD6FC0.into(),
555 )
556 }
557
558 pub fn new_with_abi_revision(name: impl Into<String>, abi_revision: AbiRevision) -> Self {
562 let name = name.into();
563
564 let artifacts_tmp = tempfile::tempdir().expect("create tempdir for package");
565 let artifacts = Utf8Path::from_path(artifacts_tmp.path())
566 .expect("checking packagedir is UTF-8")
567 .to_path_buf();
568
569 fs::create_dir(artifacts.join("contents")).expect("create /packages/contents");
570
571 let mut builder = fuchsia_pkg::PackageBuilder::new(&name, abi_revision);
572 builder.manifest_path(artifacts.join("manifest.json"));
573 builder.repository("fuchsia.com");
574 builder.manifest_blobs_relative_to(fuchsia_pkg::RelativeTo::File);
575
576 Self {
577 builder,
578 name: name.try_into().unwrap(),
579 contents: BTreeMap::new(),
580 has_subpackages: false,
581 subpackage_blobs: Some(HashMap::new()),
582 _artifacts_tmp: artifacts_tmp,
583 artifacts,
584 }
585 }
586
587 pub fn dir(mut self, path: impl Into<PathBuf>) -> PackageDir {
593 let path = path.into();
594 self.make_dirs(&path);
595 PackageDir::new(self, path)
596 }
597
598 pub fn add_resource_at(
606 mut self,
607 path: impl Into<PathBuf>,
608 mut contents: impl io::Read,
609 ) -> Self {
610 let path = path.into();
611 let path_str = path.to_str().unwrap();
612 let () = fuchsia_url::validate_resource_path(
613 path.to_str().unwrap_or_else(|| panic!("path must be utf8: {path:?}")),
614 )
615 .unwrap_or_else(|_| panic!("path must be an object relative path expression: {path:?}"));
616
617 let mut data = vec![];
618 contents.read_to_end(&mut data).unwrap();
619
620 if path.starts_with("meta/") {
621 self.builder
622 .add_contents_to_far(path_str, &data, self.artifacts.join("contents"))
623 .expect("adding meta blob to succeed");
624 } else {
625 self.builder
626 .add_contents_as_blob(path_str, &data, self.artifacts.join("contents"))
627 .expect("adding blob to succeed");
628 }
629
630 let replaced = self.contents.insert(path.clone(), PackageEntry::File(data));
631 assert_eq!(None, replaced, "already contains an entry at {path:?}");
632 self
633 }
634
635 fn make_dirs(&mut self, path: &Path) {
636 for ancestor in path.ancestors() {
637 if ancestor == Path::new("") {
638 continue;
639 }
640 assert!(
641 self.contents
642 .entry(ancestor.to_owned())
643 .or_insert(PackageEntry::Directory)
644 .is_dir(),
645 "{ancestor:?} is not a directory"
646 );
647 }
648 }
649
650 pub fn add_subpackage(
658 mut self,
659 name: impl TryInto<fuchsia_url::RelativePackageUrl>,
660 subpackage: &Package,
661 ) -> Self {
662 let name = name.try_into().map_err(|_| ()).expect("valid RelativePackageUrl");
663 let manifest_path = subpackage.artifacts().join("manifest.json").into();
664
665 self.builder.add_subpackage(&name, *subpackage.hash(), manifest_path).unwrap();
666 self.has_subpackages = true;
667
668 match (&mut self.subpackage_blobs, &subpackage.subpackage_blobs) {
669 (Some(current_blobs), Some(new_blobs)) => {
670 let (meta_far, content_blobs) = subpackage.contents();
671 current_blobs.insert(meta_far.merkle, meta_far.contents);
672 current_blobs.extend(content_blobs);
673 current_blobs.extend(new_blobs.iter().map(|(k, v)| (*k, v.clone())));
674 }
675 (Some(_), None) => self.subpackage_blobs = None,
676 (None, Some(_)) | (None, None) => {}
677 }
678
679 self
680 }
681
682 pub fn add_subpackage_by_hash(
692 mut self,
693 name: impl TryInto<fuchsia_url::RelativePackageUrl>,
694 hash: Hash,
695 ) -> Self {
696 let name = name.try_into().map_err(|_| ()).expect("valid RelativePackageUrl");
697
698 self.builder.add_subpackage(&name, hash, "".into()).unwrap();
699 self.has_subpackages = true;
700 self.subpackage_blobs = None;
701 self
702 }
703
704 pub async fn build(self) -> Result<Package, Error> {
706 let manifest = self.builder.build(&self.artifacts, self.artifacts.join("meta.far"))?;
718 let meta_far_merkle =
719 manifest.blobs().iter().find(|b| b.path == "meta/").context("finding meta/")?.merkle;
720
721 fs::remove_file(self.artifacts.join("meta/fuchsia.abi/abi-revision"))?;
723 fs::remove_dir(self.artifacts.join("meta/fuchsia.abi"))?;
724 if self.has_subpackages {
725 fs::remove_file(self.artifacts.join("meta/fuchsia.pkg/subpackages"))?;
726 fs::remove_dir(self.artifacts.join("meta/fuchsia.pkg"))?;
727 }
728 fs::remove_file(self.artifacts.join("meta/package"))?;
729 fs::remove_dir(self.artifacts.join("meta"))?;
730
731 Ok(Package {
732 name: self.name,
733 meta_far_merkle,
734 _artifacts_tmp: self._artifacts_tmp,
735 artifacts: self.artifacts,
736 subpackage_blobs: self.subpackage_blobs,
737 })
738 }
739}
740
741pub struct PackageDir {
743 pkg: PackageBuilder,
744 path: PathBuf,
745}
746
747impl PackageDir {
748 fn new(pkg: PackageBuilder, path: impl Into<PathBuf>) -> Self {
749 Self { pkg, path: path.into() }
750 }
751
752 pub fn add_resource_at(mut self, path: impl AsRef<Path>, contents: impl io::Read) -> Self {
758 self.pkg = self.pkg.add_resource_at(self.path.join(path.as_ref()), contents);
759 self
760 }
761
762 pub fn finish(self) -> PackageBuilder {
764 self.pkg
765 }
766}
767
768#[cfg(test)]
769mod tests {
770 use super::*;
771 use assert_matches::assert_matches;
772 use fuchsia_pkg::MetaPackage;
773
774 #[test]
775 #[should_panic(expected = "adding blob to succeed")]
776 fn test_panics_file_with_existing_parent_as_file() {
777 let _: Result<(), Error> = {
778 PackageBuilder::new("test")
779 .add_resource_at("data", "data contents".as_bytes())
780 .add_resource_at("data/foo", "data/foo contents".as_bytes());
781 Ok(())
782 };
783 }
784
785 #[test]
786 #[should_panic(expected = r#""data" is not a directory"#)]
787 fn test_panics_dir_with_existing_file() {
788 let _: Result<(), Error> = {
789 PackageBuilder::new("test")
790 .add_resource_at("data", "data contents".as_bytes())
791 .dir("data");
792 Ok(())
793 };
794 }
795
796 #[test]
797 #[should_panic(expected = r#""data" is not a directory"#)]
798 fn test_panics_nested_dir_with_existing_file() {
799 let _: Result<(), Error> = {
800 PackageBuilder::new("test")
801 .add_resource_at("data", "data contents".as_bytes())
802 .dir("data/foo");
803 Ok(())
804 };
805 }
806
807 #[test]
808 #[should_panic(expected = "adding blob to succeed")]
809 fn test_panics_file_with_existing_dir() {
810 let _: Result<(), Error> = {
811 PackageBuilder::new("test")
812 .dir("data")
813 .add_resource_at("foo", "data/foo contents".as_bytes())
814 .finish()
815 .add_resource_at("data", "data contents".as_bytes());
816 Ok(())
817 };
818 }
819
820 #[fuchsia_async::run_singlethreaded(test)]
821 async fn test_basic() -> Result<(), Error> {
822 let pkg = PackageBuilder::new("rolldice")
823 .dir("bin")
824 .add_resource_at("rolldice", "asldkfjaslkdfjalskdjfalskdf".as_bytes())
825 .finish()
826 .build()
827 .await?;
828
829 assert_eq!(
830 pkg.meta_far_merkle,
831 "de210ba39b8f597cc1986c37b369c990707649f63bb8fa23b244a38274018b78".parse()?
832 );
833 assert_eq!(pkg.meta_far_merkle, fuchsia_merkle::root_from_reader(pkg.meta_far()?)?);
834 assert_eq!(
835 pkg.list_blobs(),
836 BTreeSet::from([
837 "de210ba39b8f597cc1986c37b369c990707649f63bb8fa23b244a38274018b78".parse()?,
838 "b5b34f6234631edc7ccaa25533e2050e5d597a7331c8974306b617a3682a3197".parse()?
839 ])
840 );
841
842 Ok(())
843 }
844
845 #[fuchsia_async::run_singlethreaded(test)]
846 async fn test_content_blob_files() -> Result<(), Error> {
847 let pkg = PackageBuilder::new("rolldice")
848 .dir("bin")
849 .add_resource_at("rolldice", "asldkfjaslkdfjalskdjfalskdf".as_bytes())
850 .add_resource_at("rolldice2", "asldkfjaslkdfjalskdjfalskdf".as_bytes())
851 .finish()
852 .build()
853 .await?;
854
855 let mut iter = pkg.content_blob_files();
856 for _ in 0..2 {
858 let BlobFile { merkle, mut file } = iter.next().unwrap();
859 assert_eq!(
860 merkle,
861 "b5b34f6234631edc7ccaa25533e2050e5d597a7331c8974306b617a3682a3197".parse().unwrap()
862 );
863 let mut contents = vec![];
864 file.read_to_end(&mut contents).unwrap();
865 assert_eq!(contents, b"asldkfjaslkdfjalskdjfalskdf")
866 }
867 assert_eq!(iter.next().map(|b| b.merkle), None);
868
869 Ok(())
870 }
871
872 #[fuchsia_async::run_singlethreaded(test)]
873 async fn test_dir_semantics() -> Result<(), Error> {
874 let with_dir = PackageBuilder::new("data-file")
875 .dir("data")
876 .add_resource_at("file", "contents".as_bytes())
877 .finish()
878 .build()
879 .await?;
880
881 let with_direct = PackageBuilder::new("data-file")
882 .add_resource_at("data/file", "contents".as_bytes())
883 .build()
884 .await?;
885
886 assert_eq!(with_dir.hash(), with_direct.hash());
887
888 Ok(())
889 }
890
891 fn make_this_package_dir() -> Result<tempfile::TempDir, Error> {
894 let dir = tempfile::tempdir()?;
895
896 let this_package_root = Path::new("/pkg");
897
898 for entry in WalkDir::new(this_package_root) {
899 let entry = entry?;
900 let path = entry.path();
901
902 let relative_path = path.strip_prefix(this_package_root).unwrap();
903 let rebased_path = dir.path().join(relative_path);
904
905 if entry.file_type().is_dir() {
906 fs::create_dir_all(rebased_path)?;
907 } else if entry.file_type().is_file() {
908 fs::copy(path, rebased_path)?;
909 }
910 }
911
912 Ok(dir)
913 }
914
915 #[fuchsia_async::run_singlethreaded(test)]
916 async fn test_from_dir() {
917 let abi_revision = AbiRevision::from_u64(0x5836508c2defac54); let root = {
920 let dir = tempfile::tempdir().unwrap();
921
922 fs::create_dir(dir.path().join("meta")).unwrap();
923 fs::create_dir(dir.path().join("data")).unwrap();
924
925 MetaPackage::from_name_and_variant_zero("asdf".parse().unwrap())
926 .serialize(File::create(dir.path().join("meta/package")).unwrap())
927 .unwrap();
928
929 fs::create_dir(dir.path().join("meta/fuchsia.abi")).unwrap();
930 fs::write(dir.path().join("meta/fuchsia.abi/abi-revision"), abi_revision.as_bytes())
931 .unwrap();
932
933 fs::write(dir.path().join("data/hello"), "world").unwrap();
934
935 dir
936 };
937
938 let from_dir = Package::from_dir(root.path()).await.unwrap();
939
940 let pkg = PackageBuilder::new_with_abi_revision("asdf", abi_revision)
941 .add_resource_at("data/hello", "world".as_bytes())
942 .build()
943 .await
944 .unwrap();
945
946 assert_eq!(from_dir.meta_far_merkle, pkg.meta_far_merkle);
947 }
948
949 #[fuchsia_async::run_singlethreaded(test)]
950 async fn test_identity() -> Result<(), Error> {
951 let pkg = Package::identity().await.unwrap();
952
953 assert_eq!(pkg.meta_far_merkle, fuchsia_merkle::root_from_reader(pkg.meta_far()?)?);
954
955 assert_eq!(pkg.meta_far_merkle, fs::read_to_string("/pkg/meta")?.parse()?);
957
958 let this_pkg_dir = fuchsia_fs::directory::open_in_namespace("/pkg", fio::PERM_READABLE)?;
959 pkg.verify_contents(&this_pkg_dir).await.expect("contents to be equivalent");
960
961 let pkg_dir = make_this_package_dir()?;
962
963 let this_pkg_dir = fuchsia_fs::directory::open_in_namespace(
964 pkg_dir.path().to_str().unwrap(),
965 fio::PERM_READABLE,
966 )?;
967
968 assert_matches!(pkg.verify_contents(&this_pkg_dir).await, Ok(()));
969
970 Ok(())
971 }
972
973 #[fuchsia_async::run_singlethreaded(test)]
974 async fn test_verify_contents_rejects_extra_blob() -> Result<(), Error> {
975 let pkg = Package::identity().await?;
976 let pkg_dir = make_this_package_dir()?;
977
978 fs::write(pkg_dir.path().join("unexpected"), "unexpected file".as_bytes())?;
979
980 let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace(
981 pkg_dir.path().to_str().unwrap(),
982 fio::PERM_READABLE,
983 )?;
984
985 assert_matches!(
986 pkg.verify_contents(&pkg_dir_proxy).await,
987 Err(VerificationError::ExtraFile{ref path}) if path == "unexpected");
988
989 Ok(())
990 }
991
992 #[fuchsia_async::run_singlethreaded(test)]
993 async fn test_verify_contents_rejects_extra_meta_file() -> Result<(), Error> {
994 let pkg = Package::identity().await?;
995 let pkg_dir = make_this_package_dir()?;
996
997 fs::write(pkg_dir.path().join("meta/unexpected"), "unexpected file".as_bytes())?;
998
999 let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace(
1000 pkg_dir.path().to_str().unwrap(),
1001 fio::PERM_READABLE,
1002 )?;
1003
1004 assert_matches!(
1005 pkg.verify_contents(&pkg_dir_proxy).await,
1006 Err(VerificationError::ExtraFile{ref path}) if path == "meta/unexpected");
1007
1008 Ok(())
1009 }
1010
1011 #[fuchsia_async::run_singlethreaded(test)]
1012 async fn test_verify_contents_rejects_missing_blob() -> Result<(), Error> {
1013 let pkg = Package::identity().await?;
1014 let pkg_dir = make_this_package_dir()?;
1015
1016 fs::remove_file(pkg_dir.path().join("bin/fuchsia_pkg_testing_lib_test"))?;
1017
1018 let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace(
1019 pkg_dir.path().to_str().unwrap(),
1020 fio::PERM_READABLE,
1021 )?;
1022
1023 assert_matches!(
1024 pkg.verify_contents(&pkg_dir_proxy).await,
1025 Err(VerificationError::MissingFile{ref path}) if path == "bin/fuchsia_pkg_testing_lib_test");
1026
1027 Ok(())
1028 }
1029
1030 #[fuchsia_async::run_singlethreaded(test)]
1031 async fn test_verify_contents_rejects_different_contents() -> Result<(), Error> {
1032 let pkg = Package::identity().await?;
1033 let pkg_dir = make_this_package_dir()?;
1034
1035 fs::write(pkg_dir.path().join("bin/fuchsia_pkg_testing_lib_test"), "broken".as_bytes())?;
1036
1037 let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace(
1038 pkg_dir.path().to_str().unwrap(),
1039 fio::PERM_READABLE,
1040 )?;
1041
1042 assert_matches!(
1043 pkg.verify_contents(&pkg_dir_proxy).await,
1044 Err(VerificationError::DifferentFileData{ref path}) if path == "bin/fuchsia_pkg_testing_lib_test");
1045
1046 Ok(())
1047 }
1048
1049 #[fuchsia_async::run_singlethreaded(test)]
1050 async fn test_meta_subpackages_with_no_subpackages() {
1051 let pkg = PackageBuilder::new("pkg").build().await.unwrap();
1052
1053 assert!(pkg.meta_subpackages().unwrap().subpackages().is_empty());
1054 }
1055
1056 #[fuchsia_async::run_singlethreaded(test)]
1057 async fn test_add_subpackage() {
1058 let sub_sub_pkg = PackageBuilder::new("sub-sub-pkg")
1060 .add_resource_at("c-blob", "c-blob-contents".as_bytes())
1061 .build()
1062 .await
1063 .unwrap();
1064
1065 let sub_pkg = PackageBuilder::new("sub-pkg")
1066 .add_resource_at("b-blob", "b-blob-contents".as_bytes())
1067 .add_subpackage("subpackage-1", &sub_sub_pkg)
1068 .build()
1069 .await
1070 .unwrap();
1071
1072 let mut expected_subpackage_blobs = HashMap::new();
1073 let (sub_sub_pkg_meta_far, content_blobs) = sub_sub_pkg.contents();
1074 expected_subpackage_blobs
1075 .insert(sub_sub_pkg_meta_far.merkle, sub_sub_pkg_meta_far.contents);
1076 expected_subpackage_blobs
1077 .insert(content_blobs.into_keys().next().unwrap(), b"c-blob-contents".to_vec());
1078
1079 assert_eq!(*sub_pkg.subpackage_blobs().unwrap(), expected_subpackage_blobs);
1080 assert_eq!(
1081 sub_pkg.meta_subpackages().unwrap(),
1082 MetaSubpackages::from_iter([(
1083 fuchsia_url::RelativePackageUrl::parse("subpackage-1").unwrap(),
1084 sub_sub_pkg_meta_far.merkle
1085 )])
1086 );
1087 let (sub_pkg_meta_far, content_blobs) = sub_pkg.contents();
1088 let mut expected_all_blobs = content_blobs
1089 .keys()
1090 .copied()
1091 .chain([sub_pkg_meta_far.merkle])
1092 .chain(expected_subpackage_blobs.keys().copied())
1093 .collect();
1094 assert_eq!(sub_pkg.list_blobs(), expected_all_blobs);
1095
1096 let pkg = PackageBuilder::new("pkg")
1098 .add_subpackage("subpackage-0", &sub_pkg)
1099 .build()
1100 .await
1101 .unwrap();
1102
1103 expected_subpackage_blobs.insert(sub_pkg_meta_far.merkle, sub_pkg_meta_far.contents);
1104 expected_subpackage_blobs
1105 .insert(content_blobs.into_keys().next().unwrap(), b"b-blob-contents".to_vec());
1106
1107 assert_eq!(*pkg.subpackage_blobs().unwrap(), expected_subpackage_blobs);
1108 assert_eq!(
1109 pkg.meta_subpackages().unwrap(),
1110 MetaSubpackages::from_iter([(
1111 fuchsia_url::RelativePackageUrl::parse("subpackage-0").unwrap(),
1112 sub_pkg_meta_far.merkle
1113 )])
1114 );
1115 expected_all_blobs.insert(*pkg.hash());
1116 assert_eq!(pkg.list_blobs(), expected_all_blobs);
1117 }
1118
1119 #[fuchsia_async::run_singlethreaded(test)]
1120 async fn test_add_subpackage_by_hash() {
1121 let pkg = PackageBuilder::new("pkg")
1122 .add_subpackage_by_hash("subpackage-name", Hash::from([0; 32]))
1123 .build()
1124 .await
1125 .unwrap();
1126
1127 assert_eq!(pkg.subpackage_blobs(), None);
1128 assert_eq!(
1129 pkg.meta_subpackages().unwrap(),
1130 MetaSubpackages::from_iter([(
1131 fuchsia_url::RelativePackageUrl::parse("subpackage-name").unwrap(),
1132 Hash::from([0; 32])
1133 )])
1134 );
1135 }
1136}