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;
14use fuchsia_url::fuchsia_pkg::PinnedAbsolutePackageUrl;
15use futures::join;
16use futures::prelude::*;
17use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
18use std::convert::TryInto as _;
19use std::fs::{self, File};
20use std::io::{self, Read};
21use std::path::{Path, PathBuf};
22use tempfile::TempDir;
23use version_history::AbiRevision;
24use walkdir::WalkDir;
25use zx::Status;
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 pinned_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.to_owned());
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 { epitaph, .. })
387 if epitaph == 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_one(
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 =
434 file.get_backing_memory(fio::VmoFlags::READ).await?.map_err(Status::err_from_raw);
435
436 let mut expect_empty_blob = false;
437
438 match result {
440 Ok(vmo) => {
441 let size = vmo.get_content_size().context("unable to get vmo size")?;
442 let mut buf = vec![0u8; size as usize];
443 let () = vmo.read(&mut buf[..], 0).context("unable to read from vmo")?;
444 return Ok(buf);
445 }
446 Err(status) => match status {
447 Status::NOT_SUPPORTED => {}
448 Status::BAD_STATE => {
449 expect_empty_blob = true;
453 }
454 status => {
455 return Err(VerificationError::from(format_err!(
456 "unexpected error opening file buffer: {:?}",
457 status
458 )));
459 }
460 },
461 }
462
463 let mut buf = vec![];
464 loop {
465 let chunk = file
466 .read(fio::MAX_BUF)
467 .await
468 .context("file read to respond")?
469 .map_err(Status::err_from_raw)
470 .map_err(|status| VerificationError::FileReadError { path: path.into(), status })?;
471
472 if chunk.is_empty() {
473 if expect_empty_blob {
474 assert_eq!(buf, Vec::<u8>::new());
475 }
476 return Ok(buf);
477 }
478
479 buf.extend(chunk);
480 }
481 };
482
483 let (open, read) = join!(open, read);
484 let close_result = file.close().await;
485 let result = open.and(read)?;
486 let close_result = close_result.context("file close to respond")?;
488 close_result.map_err(|status| {
489 format_err!("unable to close {:?}: {:?}", path, zx::Status::err_from_raw(status))
490 })?;
491 Ok(result)
492}
493
494#[derive(Debug)]
496pub enum VerificationError {
497 ExtraFile {
499 path: String,
501 },
502 MissingFile {
504 path: String,
506 },
507 DifferentFileData {
509 path: String,
511 },
512 FileReadError {
514 path: String,
516 status: Status,
518 },
519 Other(Error),
521}
522
523impl<T: Into<Error>> From<T> for VerificationError {
524 fn from(x: T) -> Self {
525 VerificationError::Other(x.into())
526 }
527}
528
529pub struct PackageBuilder {
531 name: PackageName,
532 contents: BTreeMap<PathBuf, PackageEntry>,
533
534 has_subpackages: bool,
535 subpackage_blobs: Option<HashMap<Hash, Vec<u8>>>,
538
539 builder: fuchsia_pkg::PackageBuilder,
540 _artifacts_tmp: TempDir,
541 artifacts: Utf8PathBuf,
542}
543
544impl PackageBuilder {
545 pub fn new(name: impl Into<String>) -> Self {
553 Self::new_with_abi_revision(
554 name,
555 0xECCEA2F70ACD6FC0.into(),
557 )
558 }
559
560 pub fn new_with_abi_revision(name: impl Into<String>, abi_revision: AbiRevision) -> Self {
564 let name = name.into();
565
566 let artifacts_tmp = tempfile::tempdir().expect("create tempdir for package");
567 let artifacts = Utf8Path::from_path(artifacts_tmp.path())
568 .expect("checking packagedir is UTF-8")
569 .to_path_buf();
570
571 fs::create_dir(artifacts.join("contents")).expect("create /packages/contents");
572
573 let mut builder = fuchsia_pkg::PackageBuilder::new(&name, abi_revision);
574 builder.manifest_path(artifacts.join("manifest.json"));
575 builder.repository("fuchsia.com");
576 builder.manifest_blobs_relative_to(fuchsia_pkg::RelativeTo::File);
577
578 Self {
579 builder,
580 name: name.try_into().unwrap(),
581 contents: BTreeMap::new(),
582 has_subpackages: false,
583 subpackage_blobs: Some(HashMap::new()),
584 _artifacts_tmp: artifacts_tmp,
585 artifacts,
586 }
587 }
588
589 pub fn dir(mut self, path: impl Into<PathBuf>) -> PackageDir {
595 let path = path.into();
596 self.make_dirs(&path);
597 PackageDir::new(self, path)
598 }
599
600 pub fn add_resource_at(
608 mut self,
609 path: impl Into<PathBuf>,
610 mut contents: impl io::Read,
611 ) -> Self {
612 let path = path.into();
613 let path_str = path.to_str().unwrap();
614 let () = fuchsia_url::Resource::validate_str(
615 path.to_str().unwrap_or_else(|| panic!("path must be utf8: {path:?}")),
616 )
617 .unwrap_or_else(|_| panic!("path must be an object relative path expression: {path:?}"));
618
619 let mut data = vec![];
620 contents.read_to_end(&mut data).unwrap();
621
622 if path.starts_with("meta/") {
623 self.builder
624 .add_contents_to_far(path_str, &data, self.artifacts.join("contents"))
625 .expect("adding meta blob to succeed");
626 } else {
627 self.builder
628 .add_contents_as_blob(path_str, &data, self.artifacts.join("contents"))
629 .expect("adding blob to succeed");
630 }
631
632 let replaced = self.contents.insert(path.clone(), PackageEntry::File(data));
633 assert_eq!(None, replaced, "already contains an entry at {path:?}");
634 self
635 }
636
637 fn make_dirs(&mut self, path: &Path) {
638 for ancestor in path.ancestors() {
639 if ancestor == Path::new("") {
640 continue;
641 }
642 assert!(
643 self.contents
644 .entry(ancestor.to_owned())
645 .or_insert(PackageEntry::Directory)
646 .is_dir(),
647 "{ancestor:?} is not a directory"
648 );
649 }
650 }
651
652 pub fn add_subpackage(
660 mut self,
661 name: impl TryInto<fuchsia_url::RelativePackageUrl>,
662 subpackage: &Package,
663 ) -> Self {
664 let name = name.try_into().map_err(|_| ()).expect("valid RelativePackageUrl");
665 let manifest_path = subpackage.artifacts().join("manifest.json").into();
666
667 self.builder.add_subpackage(&name, *subpackage.hash(), manifest_path).unwrap();
668 self.has_subpackages = true;
669
670 match (&mut self.subpackage_blobs, &subpackage.subpackage_blobs) {
671 (Some(current_blobs), Some(new_blobs)) => {
672 let (meta_far, content_blobs) = subpackage.contents();
673 current_blobs.insert(meta_far.merkle, meta_far.contents);
674 current_blobs.extend(content_blobs);
675 current_blobs.extend(new_blobs.iter().map(|(k, v)| (*k, v.clone())));
676 }
677 (Some(_), None) => self.subpackage_blobs = None,
678 (None, Some(_)) | (None, None) => {}
679 }
680
681 self
682 }
683
684 pub fn add_subpackage_by_hash(
694 mut self,
695 name: impl TryInto<fuchsia_url::RelativePackageUrl>,
696 hash: Hash,
697 ) -> Self {
698 let name = name.try_into().map_err(|_| ()).expect("valid RelativePackageUrl");
699
700 self.builder.add_subpackage(&name, hash, "".into()).unwrap();
701 self.has_subpackages = true;
702 self.subpackage_blobs = None;
703 self
704 }
705
706 pub async fn build(self) -> Result<Package, Error> {
708 let manifest = self.builder.build(&self.artifacts, self.artifacts.join("meta.far"))?;
720 let meta_far_merkle =
721 manifest.blobs().iter().find(|b| b.path == "meta/").context("finding meta/")?.merkle;
722
723 fs::remove_file(self.artifacts.join("meta/fuchsia.abi/abi-revision"))?;
725 fs::remove_dir(self.artifacts.join("meta/fuchsia.abi"))?;
726 if self.has_subpackages {
727 fs::remove_file(self.artifacts.join("meta/fuchsia.pkg/subpackages"))?;
728 fs::remove_dir(self.artifacts.join("meta/fuchsia.pkg"))?;
729 }
730 fs::remove_file(self.artifacts.join("meta/package"))?;
731 fs::remove_dir(self.artifacts.join("meta"))?;
732
733 Ok(Package {
734 name: self.name,
735 meta_far_merkle,
736 _artifacts_tmp: self._artifacts_tmp,
737 artifacts: self.artifacts,
738 subpackage_blobs: self.subpackage_blobs,
739 })
740 }
741}
742
743pub struct PackageDir {
745 pkg: PackageBuilder,
746 path: PathBuf,
747}
748
749impl PackageDir {
750 fn new(pkg: PackageBuilder, path: impl Into<PathBuf>) -> Self {
751 Self { pkg, path: path.into() }
752 }
753
754 pub fn add_resource_at(mut self, path: impl AsRef<Path>, contents: impl io::Read) -> Self {
760 self.pkg = self.pkg.add_resource_at(self.path.join(path.as_ref()), contents);
761 self
762 }
763
764 pub fn finish(self) -> PackageBuilder {
766 self.pkg
767 }
768}
769
770#[cfg(test)]
771mod tests {
772 use super::*;
773 use assert_matches::assert_matches;
774 use fuchsia_pkg::MetaPackage;
775
776 #[test]
777 #[should_panic(expected = "adding blob to succeed")]
778 fn test_panics_file_with_existing_parent_as_file() {
779 let _: Result<(), Error> = {
780 PackageBuilder::new("test")
781 .add_resource_at("data", "data contents".as_bytes())
782 .add_resource_at("data/foo", "data/foo contents".as_bytes());
783 Ok(())
784 };
785 }
786
787 #[test]
788 #[should_panic(expected = r#""data" is not a directory"#)]
789 fn test_panics_dir_with_existing_file() {
790 let _: Result<(), Error> = {
791 PackageBuilder::new("test")
792 .add_resource_at("data", "data contents".as_bytes())
793 .dir("data");
794 Ok(())
795 };
796 }
797
798 #[test]
799 #[should_panic(expected = r#""data" is not a directory"#)]
800 fn test_panics_nested_dir_with_existing_file() {
801 let _: Result<(), Error> = {
802 PackageBuilder::new("test")
803 .add_resource_at("data", "data contents".as_bytes())
804 .dir("data/foo");
805 Ok(())
806 };
807 }
808
809 #[test]
810 #[should_panic(expected = "adding blob to succeed")]
811 fn test_panics_file_with_existing_dir() {
812 let _: Result<(), Error> = {
813 PackageBuilder::new("test")
814 .dir("data")
815 .add_resource_at("foo", "data/foo contents".as_bytes())
816 .finish()
817 .add_resource_at("data", "data contents".as_bytes());
818 Ok(())
819 };
820 }
821
822 #[fuchsia::test]
823 async fn test_basic() -> Result<(), Error> {
824 let pkg = PackageBuilder::new("rolldice")
825 .dir("bin")
826 .add_resource_at("rolldice", "asldkfjaslkdfjalskdjfalskdf".as_bytes())
827 .finish()
828 .build()
829 .await?;
830
831 assert_eq!(
832 pkg.meta_far_merkle,
833 "de210ba39b8f597cc1986c37b369c990707649f63bb8fa23b244a38274018b78".parse()?
834 );
835 assert_eq!(pkg.meta_far_merkle, fuchsia_merkle::root_from_reader(pkg.meta_far()?)?);
836 assert_eq!(
837 pkg.list_blobs(),
838 BTreeSet::from([
839 "de210ba39b8f597cc1986c37b369c990707649f63bb8fa23b244a38274018b78".parse()?,
840 "b5b34f6234631edc7ccaa25533e2050e5d597a7331c8974306b617a3682a3197".parse()?
841 ])
842 );
843
844 Ok(())
845 }
846
847 #[fuchsia::test]
848 async fn test_content_blob_files() -> Result<(), Error> {
849 let pkg = PackageBuilder::new("rolldice")
850 .dir("bin")
851 .add_resource_at("rolldice", "asldkfjaslkdfjalskdjfalskdf".as_bytes())
852 .add_resource_at("rolldice2", "asldkfjaslkdfjalskdjfalskdf".as_bytes())
853 .finish()
854 .build()
855 .await?;
856
857 let mut iter = pkg.content_blob_files();
858 for _ in 0..2 {
860 let BlobFile { merkle, mut file } = iter.next().unwrap();
861 assert_eq!(
862 merkle,
863 "b5b34f6234631edc7ccaa25533e2050e5d597a7331c8974306b617a3682a3197".parse().unwrap()
864 );
865 let mut contents = vec![];
866 file.read_to_end(&mut contents).unwrap();
867 assert_eq!(contents, b"asldkfjaslkdfjalskdjfalskdf")
868 }
869 assert_eq!(iter.next().map(|b| b.merkle), None);
870
871 Ok(())
872 }
873
874 #[fuchsia::test]
875 async fn test_dir_semantics() -> Result<(), Error> {
876 let with_dir = PackageBuilder::new("data-file")
877 .dir("data")
878 .add_resource_at("file", "contents".as_bytes())
879 .finish()
880 .build()
881 .await?;
882
883 let with_direct = PackageBuilder::new("data-file")
884 .add_resource_at("data/file", "contents".as_bytes())
885 .build()
886 .await?;
887
888 assert_eq!(with_dir.hash(), with_direct.hash());
889
890 Ok(())
891 }
892
893 fn make_this_package_dir() -> Result<tempfile::TempDir, Error> {
896 let dir = tempfile::tempdir()?;
897
898 let this_package_root = Path::new("/pkg");
899
900 for entry in WalkDir::new(this_package_root) {
901 let entry = entry?;
902 let path = entry.path();
903
904 let relative_path = path.strip_prefix(this_package_root).unwrap();
905 let rebased_path = dir.path().join(relative_path);
906
907 if entry.file_type().is_dir() {
908 fs::create_dir_all(rebased_path)?;
909 } else if entry.file_type().is_file() {
910 fs::copy(path, rebased_path)?;
911 }
912 }
913
914 Ok(dir)
915 }
916
917 #[fuchsia::test]
918 async fn test_from_dir() {
919 let abi_revision = AbiRevision::from_u64(0x5836508c2defac54); let root = {
922 let dir = tempfile::tempdir().unwrap();
923
924 fs::create_dir(dir.path().join("meta")).unwrap();
925 fs::create_dir(dir.path().join("data")).unwrap();
926
927 MetaPackage::from_name_and_variant_zero("asdf".parse().unwrap())
928 .serialize(File::create(dir.path().join("meta/package")).unwrap())
929 .unwrap();
930
931 fs::create_dir(dir.path().join("meta/fuchsia.abi")).unwrap();
932 fs::write(dir.path().join("meta/fuchsia.abi/abi-revision"), abi_revision.as_bytes())
933 .unwrap();
934
935 fs::write(dir.path().join("data/hello"), "world").unwrap();
936
937 dir
938 };
939
940 let from_dir = Package::from_dir(root.path()).await.unwrap();
941
942 let pkg = PackageBuilder::new_with_abi_revision("asdf", abi_revision)
943 .add_resource_at("data/hello", "world".as_bytes())
944 .build()
945 .await
946 .unwrap();
947
948 assert_eq!(from_dir.meta_far_merkle, pkg.meta_far_merkle);
949 }
950
951 #[fuchsia::test]
952 async fn test_identity() -> Result<(), Error> {
953 let pkg = Package::identity().await.unwrap();
954
955 assert_eq!(pkg.meta_far_merkle, fuchsia_merkle::root_from_reader(pkg.meta_far()?)?);
956
957 assert_eq!(pkg.meta_far_merkle, fs::read_to_string("/pkg/meta")?.parse()?);
959
960 let this_pkg_dir = fuchsia_fs::directory::open_in_namespace("/pkg", fio::PERM_READABLE)?;
961 pkg.verify_contents(&this_pkg_dir).await.expect("contents to be equivalent");
962
963 let pkg_dir = make_this_package_dir()?;
964
965 let this_pkg_dir = fuchsia_fs::directory::open_in_namespace(
966 pkg_dir.path().to_str().unwrap(),
967 fio::PERM_READABLE,
968 )?;
969
970 assert_matches!(pkg.verify_contents(&this_pkg_dir).await, Ok(()));
971
972 Ok(())
973 }
974
975 #[fuchsia::test]
976 async fn test_verify_contents_rejects_extra_blob() -> Result<(), Error> {
977 let pkg = Package::identity().await?;
978 let pkg_dir = make_this_package_dir()?;
979
980 fs::write(pkg_dir.path().join("unexpected"), "unexpected file".as_bytes())?;
981
982 let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace(
983 pkg_dir.path().to_str().unwrap(),
984 fio::PERM_READABLE,
985 )?;
986
987 assert_matches!(
988 pkg.verify_contents(&pkg_dir_proxy).await,
989 Err(VerificationError::ExtraFile{ref path}) if path == "unexpected");
990
991 Ok(())
992 }
993
994 #[fuchsia::test]
995 async fn test_verify_contents_rejects_extra_meta_file() -> Result<(), Error> {
996 let pkg = Package::identity().await?;
997 let pkg_dir = make_this_package_dir()?;
998
999 fs::write(pkg_dir.path().join("meta/unexpected"), "unexpected file".as_bytes())?;
1000
1001 let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace(
1002 pkg_dir.path().to_str().unwrap(),
1003 fio::PERM_READABLE,
1004 )?;
1005
1006 assert_matches!(
1007 pkg.verify_contents(&pkg_dir_proxy).await,
1008 Err(VerificationError::ExtraFile{ref path}) if path == "meta/unexpected");
1009
1010 Ok(())
1011 }
1012
1013 #[fuchsia::test]
1014 async fn test_verify_contents_rejects_missing_blob() -> Result<(), Error> {
1015 let pkg = Package::identity().await?;
1016 let pkg_dir = make_this_package_dir()?;
1017
1018 fs::remove_file(pkg_dir.path().join("bin/fuchsia_pkg_testing_lib_test"))?;
1019
1020 let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace(
1021 pkg_dir.path().to_str().unwrap(),
1022 fio::PERM_READABLE,
1023 )?;
1024
1025 assert_matches!(
1026 pkg.verify_contents(&pkg_dir_proxy).await,
1027 Err(VerificationError::MissingFile{ref path}) if path == "bin/fuchsia_pkg_testing_lib_test");
1028
1029 Ok(())
1030 }
1031
1032 #[fuchsia::test]
1033 async fn test_verify_contents_rejects_different_contents() -> Result<(), Error> {
1034 let pkg = Package::identity().await?;
1035 let pkg_dir = make_this_package_dir()?;
1036
1037 fs::write(pkg_dir.path().join("bin/fuchsia_pkg_testing_lib_test"), "broken".as_bytes())?;
1038
1039 let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace(
1040 pkg_dir.path().to_str().unwrap(),
1041 fio::PERM_READABLE,
1042 )?;
1043
1044 assert_matches!(
1045 pkg.verify_contents(&pkg_dir_proxy).await,
1046 Err(VerificationError::DifferentFileData{ref path}) if path == "bin/fuchsia_pkg_testing_lib_test");
1047
1048 Ok(())
1049 }
1050
1051 #[fuchsia::test]
1052 async fn test_meta_subpackages_with_no_subpackages() {
1053 let pkg = PackageBuilder::new("pkg").build().await.unwrap();
1054
1055 assert!(pkg.meta_subpackages().unwrap().subpackages().is_empty());
1056 }
1057
1058 #[fuchsia::test]
1059 async fn test_add_subpackage() {
1060 let sub_sub_pkg = PackageBuilder::new("sub-sub-pkg")
1062 .add_resource_at("c-blob", "c-blob-contents".as_bytes())
1063 .build()
1064 .await
1065 .unwrap();
1066
1067 let sub_pkg = PackageBuilder::new("sub-pkg")
1068 .add_resource_at("b-blob", "b-blob-contents".as_bytes())
1069 .add_subpackage("subpackage-1", &sub_sub_pkg)
1070 .build()
1071 .await
1072 .unwrap();
1073
1074 let mut expected_subpackage_blobs = HashMap::new();
1075 let (sub_sub_pkg_meta_far, content_blobs) = sub_sub_pkg.contents();
1076 expected_subpackage_blobs
1077 .insert(sub_sub_pkg_meta_far.merkle, sub_sub_pkg_meta_far.contents);
1078 expected_subpackage_blobs
1079 .insert(content_blobs.into_keys().next().unwrap(), b"c-blob-contents".to_vec());
1080
1081 assert_eq!(*sub_pkg.subpackage_blobs().unwrap(), expected_subpackage_blobs);
1082 assert_eq!(
1083 sub_pkg.meta_subpackages().unwrap(),
1084 MetaSubpackages::from_iter([(
1085 fuchsia_url::RelativePackageUrl::parse("subpackage-1").unwrap(),
1086 sub_sub_pkg_meta_far.merkle
1087 )])
1088 );
1089 let (sub_pkg_meta_far, content_blobs) = sub_pkg.contents();
1090 let mut expected_all_blobs = content_blobs
1091 .keys()
1092 .copied()
1093 .chain([sub_pkg_meta_far.merkle])
1094 .chain(expected_subpackage_blobs.keys().copied())
1095 .collect();
1096 assert_eq!(sub_pkg.list_blobs(), expected_all_blobs);
1097
1098 let pkg = PackageBuilder::new("pkg")
1100 .add_subpackage("subpackage-0", &sub_pkg)
1101 .build()
1102 .await
1103 .unwrap();
1104
1105 expected_subpackage_blobs.insert(sub_pkg_meta_far.merkle, sub_pkg_meta_far.contents);
1106 expected_subpackage_blobs
1107 .insert(content_blobs.into_keys().next().unwrap(), b"b-blob-contents".to_vec());
1108
1109 assert_eq!(*pkg.subpackage_blobs().unwrap(), expected_subpackage_blobs);
1110 assert_eq!(
1111 pkg.meta_subpackages().unwrap(),
1112 MetaSubpackages::from_iter([(
1113 fuchsia_url::RelativePackageUrl::parse("subpackage-0").unwrap(),
1114 sub_pkg_meta_far.merkle
1115 )])
1116 );
1117 expected_all_blobs.insert(*pkg.hash());
1118 assert_eq!(pkg.list_blobs(), expected_all_blobs);
1119 }
1120
1121 #[fuchsia::test]
1122 async fn test_add_subpackage_by_hash() {
1123 let pkg = PackageBuilder::new("pkg")
1124 .add_subpackage_by_hash("subpackage-name", Hash::from([0; 32]))
1125 .build()
1126 .await
1127 .unwrap();
1128
1129 assert_eq!(pkg.subpackage_blobs(), None);
1130 assert_eq!(
1131 pkg.meta_subpackages().unwrap(),
1132 MetaSubpackages::from_iter([(
1133 fuchsia_url::RelativePackageUrl::parse("subpackage-name").unwrap(),
1134 Hash::from([0; 32])
1135 )])
1136 );
1137 }
1138}