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