| // Copyright 2019 The Fuchsia Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| //! Test tools for building Fuchsia packages. | |
| use anyhow::{Context as _, Error, format_err}; | |
| use blobfs_ramdisk::BlobfsRamdisk; | |
| use camino::{Utf8Path, Utf8PathBuf}; | |
| use fidl_fuchsia_io as fio; | |
| use fuchsia_merkle::Hash; | |
| use fuchsia_pkg::{MetaContents, MetaSubpackages, PackageManifest}; | |
| use fuchsia_url::PackageName; | |
| use fuchsia_url::fuchsia_pkg::PinnedAbsolutePackageUrl; | |
| use futures::join; | |
| use futures::prelude::*; | |
| use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; | |
| use std::convert::TryInto as _; | |
| use std::fs::{self, File}; | |
| use std::io::{self, Read}; | |
| use std::path::{Path, PathBuf}; | |
| use tempfile::TempDir; | |
| use version_history::AbiRevision; | |
| use walkdir::WalkDir; | |
| use zx::Status; | |
| /// A package generated by a [`PackageBuilder`], suitable for assembling into a TUF repository. | |
| #[derive(Debug)] | |
| pub struct Package { | |
| name: PackageName, | |
| meta_far_merkle: Hash, | |
| _artifacts_tmp: TempDir, | |
| artifacts: Utf8PathBuf, | |
| // If None then the package has subpackages but this `Package` does not have all the blobs | |
| // needed by those subpackages. | |
| subpackage_blobs: Option<HashMap<Hash, Vec<u8>>>, | |
| } | |
| #[derive(Debug, PartialEq)] | |
| enum PackageEntry { | |
| Directory, | |
| File(Vec<u8>), | |
| } | |
| impl PackageEntry { | |
| fn is_dir(&self) -> bool { | |
| matches!(self, PackageEntry::Directory) | |
| } | |
| } | |
| pub struct BlobFile { | |
| pub merkle: fuchsia_merkle::Hash, | |
| pub file: File, | |
| } | |
| /// Contents of a Blob. | |
| pub struct BlobContents { | |
| /// Merkle hash of the blob. | |
| pub merkle: fuchsia_merkle::Hash, | |
| /// Binary contents of the blob. | |
| pub contents: Vec<u8>, | |
| } | |
| impl Package { | |
| /// The merkle root of the package's meta.far. | |
| pub fn hash(&self) -> &Hash { | |
| &self.meta_far_merkle | |
| } | |
| /// The package's meta.far. | |
| pub fn meta_far(&self) -> io::Result<File> { | |
| File::open(self.artifacts.join("meta.far")) | |
| } | |
| /// The name of the package. | |
| pub fn name(&self) -> &PackageName { | |
| &self.name | |
| } | |
| /// The pinned fuchsia-pkg url of the package on fuchsia.com. | |
| pub fn pinned_fuchsia_url(&self) -> PinnedAbsolutePackageUrl { | |
| let unpinned = format!("fuchsia-pkg://fuchsia.com/{}", self.name).parse().unwrap(); | |
| PinnedAbsolutePackageUrl::from_unpinned(unpinned, self.meta_far_merkle) | |
| } | |
| /// The directory containing the blobs contained in the package, including the meta.far. | |
| pub fn artifacts(&self) -> &Utf8Path { | |
| &self.artifacts | |
| } | |
| /// Builds and returns the package located at "/pkg" in the current namespace. | |
| pub async fn identity() -> Result<Self, Error> { | |
| Self::from_dir("/pkg").await | |
| } | |
| /// Builds and returns the package located at the given path in the current namespace. | |
| pub async fn from_dir(root: impl AsRef<Path>) -> Result<Self, Error> { | |
| let root = root.as_ref(); | |
| let package_directory = fuchsia_pkg::PackageDirectory::from_proxy( | |
| fuchsia_fs::directory::open_in_namespace(root.to_str().unwrap(), fio::PERM_READABLE)?, | |
| ); | |
| let meta_package = package_directory.meta_package().await.context("read meta/package")?; | |
| let abi_revision = package_directory.abi_revision().await.context("read abi revision")?; | |
| let mut pkg = | |
| PackageBuilder::new_with_abi_revision(meta_package.name().as_ref(), abi_revision); | |
| fn is_generated_file(path: &Path) -> bool { | |
| matches!( | |
| path.to_str(), | |
| Some("meta/contents") | |
| | Some("meta/package") | |
| | Some(AbiRevision::PATH) | |
| | Some(MetaSubpackages::PATH) | |
| ) | |
| } | |
| // Add all non-generated files from this package into `pkg`. | |
| for entry in WalkDir::new(root) { | |
| let entry = entry?; | |
| let path = entry.path(); | |
| if !entry.file_type().is_file() || is_generated_file(path.strip_prefix(root).unwrap()) { | |
| continue; | |
| } | |
| let relative_path = path.strip_prefix(root).unwrap(); | |
| let f = File::open(path).context("open package blob")?; | |
| pkg = pkg.add_resource_at(relative_path.to_str().unwrap(), f); | |
| } | |
| let subpackages = package_directory | |
| .meta_subpackages() | |
| .await | |
| .context("read meta subpackages")? | |
| .into_subpackages(); | |
| if !subpackages.is_empty() { | |
| for (name, hash) in subpackages.into_iter() { | |
| pkg = pkg.add_subpackage_by_hash(name, hash); | |
| } | |
| } | |
| pkg.build().await | |
| } | |
| /// Returns the parsed contents of the meta/contents file. | |
| pub fn meta_contents(&self) -> Result<MetaContents, Error> { | |
| let mut raw_meta_far = self.meta_far()?; | |
| let mut meta_far = fuchsia_archive::Utf8Reader::new(&mut raw_meta_far)?; | |
| let raw_meta_contents = meta_far.read_file("meta/contents")?; | |
| Ok(MetaContents::deserialize(raw_meta_contents.as_slice())?) | |
| } | |
| /// Returns the parsed contents of the subpackages manifest. | |
| pub fn meta_subpackages(&self) -> Result<MetaSubpackages, Error> { | |
| let mut raw_meta_far = self.meta_far()?; | |
| let mut meta_far = fuchsia_archive::Utf8Reader::new(&mut raw_meta_far)?; | |
| Ok(match meta_far.read_file(MetaSubpackages::PATH) { | |
| Ok(bytes) => MetaSubpackages::deserialize(std::io::BufReader::new(bytes.as_slice()))?, | |
| Err(fuchsia_archive::Error::PathNotPresent(_)) => MetaSubpackages::default(), | |
| Err(e) => Err(e)?, | |
| }) | |
| } | |
| /// Returns a set of all unique blobs contained in this package, including meta.far and | |
| /// subpackage blobs. | |
| /// | |
| /// # Panics | |
| /// If either there are unknown subpackage blobs or there is an error reading meta/contents. | |
| pub fn list_blobs(&self) -> BTreeSet<Hash> { | |
| self.meta_contents() | |
| .expect("loading meta/contents") | |
| .into_hashes_undeduplicated() | |
| .chain([self.meta_far_merkle]) | |
| .chain( | |
| self.subpackage_blobs | |
| .as_ref() | |
| .unwrap_or_else(|| { | |
| panic!( | |
| "cannot list blobs for package {} with unknown subpackage blobs", | |
| self.name() | |
| ) | |
| }) | |
| .keys() | |
| .copied(), | |
| ) | |
| .collect() | |
| } | |
| /// Returns an iterator of merkle/File pairs for each content blob in the package. | |
| /// | |
| /// Does not include the meta.far, see `meta_far()` and `meta_far_merkle_root()`, instead. | |
| pub fn content_blob_files(&self) -> impl Iterator<Item = BlobFile> { | |
| let manifest = | |
| fuchsia_pkg::PackageManifest::try_load_from(self.artifacts().join("manifest.json")) | |
| .unwrap(); | |
| struct Blob { | |
| merkle: fuchsia_merkle::Hash, | |
| path: Utf8PathBuf, | |
| } | |
| #[allow(clippy::needless_collect)] | |
| let blobs = manifest | |
| .into_blobs() | |
| .into_iter() | |
| .filter(|blob| blob.path != PackageManifest::META_FAR_BLOB_PATH) | |
| .map(|blob| Blob { | |
| merkle: blob.merkle, | |
| path: self.artifacts().join(&blob.source_path), | |
| }) | |
| .collect::<Vec<_>>(); | |
| blobs | |
| .into_iter() | |
| .map(|blob| BlobFile { merkle: blob.merkle, file: File::open(blob.path).unwrap() }) | |
| } | |
| /// Returns a tuple of the contents of the meta far and the contents of all content blobs in the package. | |
| pub fn contents(&self) -> (BlobContents, HashMap<Hash, Vec<u8>>) { | |
| ( | |
| BlobContents { | |
| merkle: self.meta_far_merkle, | |
| contents: io::BufReader::new(self.meta_far().unwrap()) | |
| .bytes() | |
| .collect::<Result<Vec<u8>, _>>() | |
| .unwrap(), | |
| }, | |
| self.content_blob_files() | |
| .map(|blob_file| { | |
| ( | |
| blob_file.merkle, | |
| io::BufReader::new(blob_file.file) | |
| .bytes() | |
| .collect::<Result<Vec<u8>, _>>() | |
| .unwrap(), | |
| ) | |
| }) | |
| .collect(), | |
| ) | |
| } | |
| /// Returns None if this `Package` has subpackages but doesn't have the blobs. | |
| pub fn content_and_subpackage_blobs(&self) -> Option<HashMap<Hash, Vec<u8>>> { | |
| if let Some(subpackage_blobs) = &self.subpackage_blobs { | |
| let mut subpackage_blobs = subpackage_blobs.clone(); | |
| subpackage_blobs.extend(self.content_blob_files().map(|blob_file| { | |
| ( | |
| blob_file.merkle, | |
| io::BufReader::new(blob_file.file) | |
| .bytes() | |
| .collect::<Result<Vec<u8>, _>>() | |
| .unwrap(), | |
| ) | |
| })); | |
| Some(subpackage_blobs) | |
| } else { | |
| None | |
| } | |
| } | |
| /// Writes the meta.far and all content blobs to blobfs. | |
| /// Does not write the subpackage blobs, if any. | |
| pub async fn write_to_blobfs_ignore_subpackages(&self, blobfs_ramdisk: &BlobfsRamdisk) { | |
| fn read_file(file: &std::fs::File) -> Vec<u8> { | |
| let mut ret = vec![]; | |
| std::io::BufReader::new(file).read_to_end(&mut ret).unwrap(); | |
| ret | |
| } | |
| blobfs_ramdisk | |
| .write_blob(*self.hash(), &read_file(&self.meta_far().unwrap())) | |
| .await | |
| .expect("write_blob failed"); | |
| for blob in self.content_blob_files() { | |
| blobfs_ramdisk | |
| .write_blob(blob.merkle, &read_file(&blob.file)) | |
| .await | |
| .expect("write_blob failed"); | |
| } | |
| } | |
| /// Writes the meta.far and all content and subpackage blobs to blobfs. | |
| pub async fn write_to_blobfs(&self, blobfs_ramdisk: &BlobfsRamdisk) { | |
| let subpackage_blobs = self | |
| .subpackage_blobs | |
| .as_ref() | |
| .expect("package must know the subpackage blobs to write them"); | |
| let () = self.write_to_blobfs_ignore_subpackages(blobfs_ramdisk).await; | |
| for (hash, content) in subpackage_blobs { | |
| blobfs_ramdisk.write_blob(*hash, content).await.expect("write_blob failed"); | |
| } | |
| } | |
| /// Verifies that the given directory serves the contents of this package. | |
| pub async fn verify_contents( | |
| &self, | |
| dir: &fio::DirectoryProxy, | |
| ) -> Result<(), VerificationError> { | |
| let mut raw_meta_far = self.meta_far()?; | |
| let mut meta_far = fuchsia_archive::Utf8Reader::new(&mut raw_meta_far)?; | |
| let mut expected_paths = HashSet::new(); | |
| // Verify all entries referenced by meta/contents exist and have the correct merkle root. | |
| let raw_meta_contents = meta_far.read_file("meta/contents")?; | |
| let meta_contents = MetaContents::deserialize(raw_meta_contents.as_slice())?; | |
| for (path, merkle) in meta_contents.contents() { | |
| let actual_merkle = fuchsia_merkle::root_from_slice(read_file(dir, path).await?); | |
| if merkle != &actual_merkle { | |
| return Err(VerificationError::DifferentFileData { path: path.to_owned() }); | |
| } | |
| expected_paths.insert(path.to_owned()); | |
| } | |
| // Verify all entries in the meta FAR exist and have the correct contents. | |
| for path in meta_far.list().map(|e| e.path().to_string()).collect::<Vec<_>>() { | |
| if read_file(dir, path.as_str()).await? != meta_far.read_file(path.as_str())? { | |
| return Err(VerificationError::DifferentFileData { path }); | |
| } | |
| expected_paths.insert(path); | |
| } | |
| // Verify no other entries exist in the served directory. | |
| let mut stream = fuchsia_fs::directory::readdir_recursive(dir, /*timeout=*/ None); | |
| while let Some(entry) = stream.try_next().await? { | |
| let path = entry.name; | |
| if !expected_paths.contains(path.as_str()) { | |
| return Err(VerificationError::ExtraFile { path }); | |
| } | |
| } | |
| Ok(()) | |
| } | |
| /// The blobs used by all of the subpackages of this package (recursively). | |
| /// If None, this `Package` has subpackages but does not have the blobs needed by those | |
| /// subpackages. | |
| pub fn subpackage_blobs(&self) -> Option<&HashMap<Hash, Vec<u8>>> { | |
| self.subpackage_blobs.as_ref() | |
| } | |
| } | |
| async fn read_file(dir: &fio::DirectoryProxy, path: &str) -> Result<Vec<u8>, VerificationError> { | |
| let (file, server_end) = fidl::endpoints::create_proxy::<fio::FileMarker>(); | |
| let flags = fio::Flags::FLAG_SEND_REPRESENTATION | fio::PERM_READABLE; | |
| dir.open(path, flags, &fio::Options::default(), server_end.into_channel()) | |
| .expect("open3 request failed to send"); | |
| let mut events = file.take_event_stream(); | |
| let open = async move { | |
| let event = match events.next().await.expect("Some(event)") { | |
| Ok(representation) => match representation { | |
| fio::FileEvent::OnOpen_ { s, info } => { | |
| match Status::ok(s) { | |
| Err(Status::NOT_FOUND) => { | |
| Err(VerificationError::MissingFile { path: path.to_owned() }) | |
| } | |
| Err(status) => { | |
| Err(format_err!("unable to open {:?}: {:?}", path, status).into()) | |
| } | |
| Ok(()) => Ok(()), | |
| }?; | |
| match *info.expect("fio::FileEvent to have fio::NodeInfoDeprecated") { | |
| fio::NodeInfoDeprecated::File(fio::FileObject { event, .. }) => event, | |
| other => { | |
| panic!( | |
| "fio::NodeInfoDeprecated from fio::FileEventStream to be File variant with event: {other:?}" | |
| ) | |
| } | |
| } | |
| } | |
| fio::FileEvent::OnRepresentation { payload } => match payload { | |
| fio::Representation::File(fio::FileInfo { observer, .. }) => observer, | |
| other => { | |
| panic!( | |
| "ConnectionInfo from fio::FileEventStream to be File variant with event: {other:?}" | |
| ) | |
| } | |
| }, | |
| fio::FileEvent::_UnknownEvent { ordinal, .. } => { | |
| panic!("unknown file event {ordinal}") | |
| } | |
| }, | |
| // If not found, Open3 will send an epitaph when closing the channel. | |
| Err(fidl::Error::ClientChannelClosed { epitaph, .. }) | |
| if epitaph == zx::Status::NOT_FOUND => | |
| { | |
| return Err(VerificationError::MissingFile { path: path.to_owned() }); | |
| } | |
| Err(other_e) => { | |
| return Err( | |
| format_err!("open fidl request at {:?} failed: {:?}", path, other_e).into() | |
| ); | |
| } | |
| }; | |
| // Files served by the package will either provide an event in its describe info (if that | |
| // file is actually a blob from blobfs) or not provide an event (if that file is, for | |
| // example, a file contained within the meta far being served in the meta/ directory). | |
| // | |
| // If the file is a blobfs blob, we want to make sure it is readable. We can just try to | |
| // read from it, but the preferred method to wait for a blobfs blob to become readable is | |
| // to wait on the USER_0 signal to become asserted on the file's event. | |
| // | |
| // As all blobs served by a package should already be readable, we assert that USER_0 is | |
| // already asserted on the event. | |
| if let Some(event) = event { | |
| match event | |
| .wait_one( | |
| zx::Signals::USER_0, | |
| zx::MonotonicInstant::after(zx::MonotonicDuration::from_seconds(0)), | |
| ) | |
| .to_result() | |
| { | |
| Err(Status::TIMED_OUT) => Err(VerificationError::from(format_err!( | |
| "file served by blobfs is not complete/readable as USER_0 signal was not set on the File's event: {}", | |
| path | |
| ))), | |
| Err(other_status) => Err(VerificationError::from(format_err!( | |
| "wait_handle failed with status: {:?} {:?}", | |
| other_status, | |
| path | |
| ))), | |
| Ok(_) => Ok(()), | |
| } | |
| } else { | |
| Ok(()) | |
| } | |
| }; | |
| let read = async { | |
| let result = | |
| file.get_backing_memory(fio::VmoFlags::READ).await?.map_err(Status::err_from_raw); | |
| let mut expect_empty_blob = false; | |
| // Attempt to get the backing VMO, which is faster. Fall back to reading over FIDL | |
| match result { | |
| Ok(vmo) => { | |
| let size = vmo.get_content_size().context("unable to get vmo size")?; | |
| let mut buf = vec![0u8; size as usize]; | |
| let () = vmo.read(&mut buf[..], 0).context("unable to read from vmo")?; | |
| return Ok(buf); | |
| } | |
| Err(status) => match status { | |
| Status::NOT_SUPPORTED => {} | |
| Status::BAD_STATE => { | |
| // may or may not be intended behavior, but the empty blob will not provide a vmo, | |
| // failing with BAD_STATE. Verify in the read path below that the blob is indeed | |
| // zero length if this happens. | |
| expect_empty_blob = true; | |
| } | |
| status => { | |
| return Err(VerificationError::from(format_err!( | |
| "unexpected error opening file buffer: {:?}", | |
| status | |
| ))); | |
| } | |
| }, | |
| } | |
| let mut buf = vec![]; | |
| loop { | |
| let chunk = file | |
| .read(fio::MAX_BUF) | |
| .await | |
| .context("file read to respond")? | |
| .map_err(Status::err_from_raw) | |
| .map_err(|status| VerificationError::FileReadError { path: path.into(), status })?; | |
| if chunk.is_empty() { | |
| if expect_empty_blob { | |
| assert_eq!(buf, Vec::<u8>::new()); | |
| } | |
| return Ok(buf); | |
| } | |
| buf.extend(chunk); | |
| } | |
| }; | |
| let (open, read) = join!(open, read); | |
| let close_result = file.close().await; | |
| let result = open.and(read)?; | |
| // Only check close_result if everything that came before it looks good. | |
| let close_result = close_result.context("file close to respond")?; | |
| close_result.map_err(|status| { | |
| format_err!("unable to close {:?}: {:?}", path, zx::Status::err_from_raw(status)) | |
| })?; | |
| Ok(result) | |
| } | |
| /// An error that can occur while verifying the contents of a directory. | |
| #[derive(Debug)] | |
| pub enum VerificationError { | |
| /// The directory is serving a file that isn't in the package. | |
| ExtraFile { | |
| /// Path to the extra file. | |
| path: String, | |
| }, | |
| /// The directory is not serving a particular file that it should be serving. | |
| MissingFile { | |
| /// Path to the missing file. | |
| path: String, | |
| }, | |
| /// The actual merkle of the file does not match the merkle listed in the meta FAR. | |
| DifferentFileData { | |
| /// Path to the file. | |
| path: String, | |
| }, | |
| /// Read method on file failed. | |
| FileReadError { | |
| /// Path to the file | |
| path: String, | |
| /// Read result | |
| status: Status, | |
| }, | |
| /// Anything else. | |
| Other(Error), | |
| } | |
| impl<T: Into<Error>> From<T> for VerificationError { | |
| fn from(x: T) -> Self { | |
| VerificationError::Other(x.into()) | |
| } | |
| } | |
| /// A builder to simplify construction of Fuchsia packages. | |
| pub struct PackageBuilder { | |
| name: PackageName, | |
| contents: BTreeMap<PathBuf, PackageEntry>, | |
| has_subpackages: bool, | |
| // If None the package has subpackages but this `PackageBuilder` does not have the blobs | |
| // needed by those subpackages. | |
| subpackage_blobs: Option<HashMap<Hash, Vec<u8>>>, | |
| builder: fuchsia_pkg::PackageBuilder, | |
| _artifacts_tmp: TempDir, | |
| artifacts: Utf8PathBuf, | |
| } | |
| impl PackageBuilder { | |
| /// Creates a new `PackageBuilder`. | |
| /// | |
| /// # Panics | |
| /// | |
| /// Panics if either: | |
| /// * `name` is an invalid package name. | |
| /// * Creating a tempdir fails. | |
| pub fn new(name: impl Into<String>) -> Self { | |
| Self::new_with_abi_revision( | |
| name, | |
| // Default to ABI revision for API level 7. | |
| 0xECCEA2F70ACD6FC0.into(), | |
| ) | |
| } | |
| /// Creates a new `PackageBuilder`, just like `PackageBuilder::new()`, but | |
| /// allowing the caller to specify the ABI revision with which to stamp the | |
| /// test package. | |
| pub fn new_with_abi_revision(name: impl Into<String>, abi_revision: AbiRevision) -> Self { | |
| let name = name.into(); | |
| let artifacts_tmp = tempfile::tempdir().expect("create tempdir for package"); | |
| let artifacts = Utf8Path::from_path(artifacts_tmp.path()) | |
| .expect("checking packagedir is UTF-8") | |
| .to_path_buf(); | |
| fs::create_dir(artifacts.join("contents")).expect("create /packages/contents"); | |
| let mut builder = fuchsia_pkg::PackageBuilder::new(&name, abi_revision); | |
| builder.manifest_path(artifacts.join("manifest.json")); | |
| builder.repository("fuchsia.com"); | |
| builder.manifest_blobs_relative_to(fuchsia_pkg::RelativeTo::File); | |
| Self { | |
| builder, | |
| name: name.try_into().unwrap(), | |
| contents: BTreeMap::new(), | |
| has_subpackages: false, | |
| subpackage_blobs: Some(HashMap::new()), | |
| _artifacts_tmp: artifacts_tmp, | |
| artifacts, | |
| } | |
| } | |
| /// Create a subdirectory within the package. | |
| /// | |
| /// # Panics | |
| /// | |
| /// Panics if the package contains a file entry at `path` or any of its ancestors. | |
| pub fn dir(mut self, path: impl Into<PathBuf>) -> PackageDir { | |
| let path = path.into(); | |
| self.make_dirs(&path); | |
| PackageDir::new(self, path) | |
| } | |
| /// Adds the provided `contents` to the package at the given `path`. | |
| /// | |
| /// # Panics | |
| /// | |
| /// Panics if either: | |
| /// * The package already contains a file or directory at `path`. | |
| /// * The package contains a file at any of `path`'s ancestors. | |
| pub fn add_resource_at( | |
| mut self, | |
| path: impl Into<PathBuf>, | |
| mut contents: impl io::Read, | |
| ) -> Self { | |
| let path = path.into(); | |
| let path_str = path.to_str().unwrap(); | |
| let () = fuchsia_url::Resource::validate_str( | |
| path.to_str().unwrap_or_else(|| panic!("path must be utf8: {path:?}")), | |
| ) | |
| .unwrap_or_else(|_| panic!("path must be an object relative path expression: {path:?}")); | |
| let mut data = vec![]; | |
| contents.read_to_end(&mut data).unwrap(); | |
| if path.starts_with("meta/") { | |
| self.builder | |
| .add_contents_to_far(path_str, &data, self.artifacts.join("contents")) | |
| .expect("adding meta blob to succeed"); | |
| } else { | |
| self.builder | |
| .add_contents_as_blob(path_str, &data, self.artifacts.join("contents")) | |
| .expect("adding blob to succeed"); | |
| } | |
| let replaced = self.contents.insert(path.clone(), PackageEntry::File(data)); | |
| assert_eq!(None, replaced, "already contains an entry at {path:?}"); | |
| self | |
| } | |
| fn make_dirs(&mut self, path: &Path) { | |
| for ancestor in path.ancestors() { | |
| if ancestor == Path::new("") { | |
| continue; | |
| } | |
| assert!( | |
| self.contents | |
| .entry(ancestor.to_owned()) | |
| .or_insert(PackageEntry::Directory) | |
| .is_dir(), | |
| "{ancestor:?} is not a directory" | |
| ); | |
| } | |
| } | |
| /// Adds the provided `subpackage` to the package with name `name`. | |
| /// | |
| /// # Panics | |
| /// | |
| /// Panics if either: | |
| /// * `name` is not a valid RelativePackageUrl | |
| /// * There is already a subpackage called `name` | |
| pub fn add_subpackage( | |
| mut self, | |
| name: impl TryInto<fuchsia_url::RelativePackageUrl>, | |
| subpackage: &Package, | |
| ) -> Self { | |
| let name = name.try_into().map_err(|_| ()).expect("valid RelativePackageUrl"); | |
| let manifest_path = subpackage.artifacts().join("manifest.json").into(); | |
| self.builder.add_subpackage(&name, *subpackage.hash(), manifest_path).unwrap(); | |
| self.has_subpackages = true; | |
| match (&mut self.subpackage_blobs, &subpackage.subpackage_blobs) { | |
| (Some(current_blobs), Some(new_blobs)) => { | |
| let (meta_far, content_blobs) = subpackage.contents(); | |
| current_blobs.insert(meta_far.merkle, meta_far.contents); | |
| current_blobs.extend(content_blobs); | |
| current_blobs.extend(new_blobs.iter().map(|(k, v)| (*k, v.clone()))); | |
| } | |
| (Some(_), None) => self.subpackage_blobs = None, | |
| (None, Some(_)) | (None, None) => {} | |
| } | |
| self | |
| } | |
| /// Adds a subpackage with name `name` and hash `hash` to the package. | |
| /// Because the blobs of the subpackage are not provided, the `Package` built from this | |
| /// `PackageBuilder` will not have the subpackage blobs. | |
| /// | |
| /// # Panics | |
| /// | |
| /// Panics if either: | |
| /// * `name` is not a valid RelativePackageUrl | |
| /// * There is already a subpackage called `name` | |
| pub fn add_subpackage_by_hash( | |
| mut self, | |
| name: impl TryInto<fuchsia_url::RelativePackageUrl>, | |
| hash: Hash, | |
| ) -> Self { | |
| let name = name.try_into().map_err(|_| ()).expect("valid RelativePackageUrl"); | |
| self.builder.add_subpackage(&name, hash, "".into()).unwrap(); | |
| self.has_subpackages = true; | |
| self.subpackage_blobs = None; | |
| self | |
| } | |
| /// Builds the package. | |
| pub async fn build(self) -> Result<Package, Error> { | |
| // self.artifacts contains outputs from package creation (manifest.json/meta.far) as well | |
| // as all blobs contained in the package. | |
| // | |
| // Layout of self.artifacts: | |
| // - manifest.json | |
| // - meta.far | |
| // - contents/ | |
| // - meta/ | |
| // - non-generated meta.far files | |
| // - file/dir{N} | |
| let manifest = self.builder.build(&self.artifacts, self.artifacts.join("meta.far"))?; | |
| let meta_far_merkle = | |
| manifest.blobs().iter().find(|b| b.path == "meta/").context("finding meta/")?.merkle; | |
| // clean up after ourselves | |
| fs::remove_file(self.artifacts.join("meta/fuchsia.abi/abi-revision"))?; | |
| fs::remove_dir(self.artifacts.join("meta/fuchsia.abi"))?; | |
| if self.has_subpackages { | |
| fs::remove_file(self.artifacts.join("meta/fuchsia.pkg/subpackages"))?; | |
| fs::remove_dir(self.artifacts.join("meta/fuchsia.pkg"))?; | |
| } | |
| fs::remove_file(self.artifacts.join("meta/package"))?; | |
| fs::remove_dir(self.artifacts.join("meta"))?; | |
| Ok(Package { | |
| name: self.name, | |
| meta_far_merkle, | |
| _artifacts_tmp: self._artifacts_tmp, | |
| artifacts: self.artifacts, | |
| subpackage_blobs: self.subpackage_blobs, | |
| }) | |
| } | |
| } | |
| /// A subdirectory of a package being built. | |
| pub struct PackageDir { | |
| pkg: PackageBuilder, | |
| path: PathBuf, | |
| } | |
| impl PackageDir { | |
| fn new(pkg: PackageBuilder, path: impl Into<PathBuf>) -> Self { | |
| Self { pkg, path: path.into() } | |
| } | |
| /// Adds the provided `contents` to the package at the given `path`, relative to this | |
| /// `PackageDir`. | |
| /// | |
| /// # Panics | |
| /// If the package already contains a resource at `path`, relative to this `PackageDir`. | |
| pub fn add_resource_at(mut self, path: impl AsRef<Path>, contents: impl io::Read) -> Self { | |
| self.pkg = self.pkg.add_resource_at(self.path.join(path.as_ref()), contents); | |
| self | |
| } | |
| /// Finish adding resources to this directory, returning the modified [`PackageBuilder`]. | |
| pub fn finish(self) -> PackageBuilder { | |
| self.pkg | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| use assert_matches::assert_matches; | |
| use fuchsia_pkg::MetaPackage; | |
| #[test] | |
| #[should_panic(expected = "adding blob to succeed")] | |
| fn test_panics_file_with_existing_parent_as_file() { | |
| let _: Result<(), Error> = { | |
| PackageBuilder::new("test") | |
| .add_resource_at("data", "data contents".as_bytes()) | |
| .add_resource_at("data/foo", "data/foo contents".as_bytes()); | |
| Ok(()) | |
| }; | |
| } | |
| #[test] | |
| #[should_panic(expected = r#""data" is not a directory"#)] | |
| fn test_panics_dir_with_existing_file() { | |
| let _: Result<(), Error> = { | |
| PackageBuilder::new("test") | |
| .add_resource_at("data", "data contents".as_bytes()) | |
| .dir("data"); | |
| Ok(()) | |
| }; | |
| } | |
| #[test] | |
| #[should_panic(expected = r#""data" is not a directory"#)] | |
| fn test_panics_nested_dir_with_existing_file() { | |
| let _: Result<(), Error> = { | |
| PackageBuilder::new("test") | |
| .add_resource_at("data", "data contents".as_bytes()) | |
| .dir("data/foo"); | |
| Ok(()) | |
| }; | |
| } | |
| #[test] | |
| #[should_panic(expected = "adding blob to succeed")] | |
| fn test_panics_file_with_existing_dir() { | |
| let _: Result<(), Error> = { | |
| PackageBuilder::new("test") | |
| .dir("data") | |
| .add_resource_at("foo", "data/foo contents".as_bytes()) | |
| .finish() | |
| .add_resource_at("data", "data contents".as_bytes()); | |
| Ok(()) | |
| }; | |
| } | |
| #[fuchsia::test] | |
| async fn test_basic() -> Result<(), Error> { | |
| let pkg = PackageBuilder::new("rolldice") | |
| .dir("bin") | |
| .add_resource_at("rolldice", "asldkfjaslkdfjalskdjfalskdf".as_bytes()) | |
| .finish() | |
| .build() | |
| .await?; | |
| assert_eq!( | |
| pkg.meta_far_merkle, | |
| "de210ba39b8f597cc1986c37b369c990707649f63bb8fa23b244a38274018b78".parse()? | |
| ); | |
| assert_eq!(pkg.meta_far_merkle, fuchsia_merkle::root_from_reader(pkg.meta_far()?)?); | |
| assert_eq!( | |
| pkg.list_blobs(), | |
| BTreeSet::from([ | |
| "de210ba39b8f597cc1986c37b369c990707649f63bb8fa23b244a38274018b78".parse()?, | |
| "b5b34f6234631edc7ccaa25533e2050e5d597a7331c8974306b617a3682a3197".parse()? | |
| ]) | |
| ); | |
| Ok(()) | |
| } | |
| #[fuchsia::test] | |
| async fn test_content_blob_files() -> Result<(), Error> { | |
| let pkg = PackageBuilder::new("rolldice") | |
| .dir("bin") | |
| .add_resource_at("rolldice", "asldkfjaslkdfjalskdjfalskdf".as_bytes()) | |
| .add_resource_at("rolldice2", "asldkfjaslkdfjalskdjfalskdf".as_bytes()) | |
| .finish() | |
| .build() | |
| .await?; | |
| let mut iter = pkg.content_blob_files(); | |
| // 2 identical entries | |
| for _ in 0..2 { | |
| let BlobFile { merkle, mut file } = iter.next().unwrap(); | |
| assert_eq!( | |
| merkle, | |
| "b5b34f6234631edc7ccaa25533e2050e5d597a7331c8974306b617a3682a3197".parse().unwrap() | |
| ); | |
| let mut contents = vec![]; | |
| file.read_to_end(&mut contents).unwrap(); | |
| assert_eq!(contents, b"asldkfjaslkdfjalskdjfalskdf") | |
| } | |
| assert_eq!(iter.next().map(|b| b.merkle), None); | |
| Ok(()) | |
| } | |
| #[fuchsia::test] | |
| async fn test_dir_semantics() -> Result<(), Error> { | |
| let with_dir = PackageBuilder::new("data-file") | |
| .dir("data") | |
| .add_resource_at("file", "contents".as_bytes()) | |
| .finish() | |
| .build() | |
| .await?; | |
| let with_direct = PackageBuilder::new("data-file") | |
| .add_resource_at("data/file", "contents".as_bytes()) | |
| .build() | |
| .await?; | |
| assert_eq!(with_dir.hash(), with_direct.hash()); | |
| Ok(()) | |
| } | |
| /// Creates a clone of the contents of /pkg in a tempdir so that tests can manipulate its | |
| /// contents. | |
| fn make_this_package_dir() -> Result<tempfile::TempDir, Error> { | |
| let dir = tempfile::tempdir()?; | |
| let this_package_root = Path::new("/pkg"); | |
| for entry in WalkDir::new(this_package_root) { | |
| let entry = entry?; | |
| let path = entry.path(); | |
| let relative_path = path.strip_prefix(this_package_root).unwrap(); | |
| let rebased_path = dir.path().join(relative_path); | |
| if entry.file_type().is_dir() { | |
| fs::create_dir_all(rebased_path)?; | |
| } else if entry.file_type().is_file() { | |
| fs::copy(path, rebased_path)?; | |
| } | |
| } | |
| Ok(dir) | |
| } | |
| #[fuchsia::test] | |
| async fn test_from_dir() { | |
| let abi_revision = AbiRevision::from_u64(0x5836508c2defac54); // Random value. | |
| let root = { | |
| let dir = tempfile::tempdir().unwrap(); | |
| fs::create_dir(dir.path().join("meta")).unwrap(); | |
| fs::create_dir(dir.path().join("data")).unwrap(); | |
| MetaPackage::from_name_and_variant_zero("asdf".parse().unwrap()) | |
| .serialize(File::create(dir.path().join("meta/package")).unwrap()) | |
| .unwrap(); | |
| fs::create_dir(dir.path().join("meta/fuchsia.abi")).unwrap(); | |
| fs::write(dir.path().join("meta/fuchsia.abi/abi-revision"), abi_revision.as_bytes()) | |
| .unwrap(); | |
| fs::write(dir.path().join("data/hello"), "world").unwrap(); | |
| dir | |
| }; | |
| let from_dir = Package::from_dir(root.path()).await.unwrap(); | |
| let pkg = PackageBuilder::new_with_abi_revision("asdf", abi_revision) | |
| .add_resource_at("data/hello", "world".as_bytes()) | |
| .build() | |
| .await | |
| .unwrap(); | |
| assert_eq!(from_dir.meta_far_merkle, pkg.meta_far_merkle); | |
| } | |
| #[fuchsia::test] | |
| async fn test_identity() -> Result<(), Error> { | |
| let pkg = Package::identity().await.unwrap(); | |
| assert_eq!(pkg.meta_far_merkle, fuchsia_merkle::root_from_reader(pkg.meta_far()?)?); | |
| // Verify the generated package's merkle root is the same as this test package's merkle root. | |
| assert_eq!(pkg.meta_far_merkle, fs::read_to_string("/pkg/meta")?.parse()?); | |
| let this_pkg_dir = fuchsia_fs::directory::open_in_namespace("/pkg", fio::PERM_READABLE)?; | |
| pkg.verify_contents(&this_pkg_dir).await.expect("contents to be equivalent"); | |
| let pkg_dir = make_this_package_dir()?; | |
| let this_pkg_dir = fuchsia_fs::directory::open_in_namespace( | |
| pkg_dir.path().to_str().unwrap(), | |
| fio::PERM_READABLE, | |
| )?; | |
| assert_matches!(pkg.verify_contents(&this_pkg_dir).await, Ok(())); | |
| Ok(()) | |
| } | |
| #[fuchsia::test] | |
| async fn test_verify_contents_rejects_extra_blob() -> Result<(), Error> { | |
| let pkg = Package::identity().await?; | |
| let pkg_dir = make_this_package_dir()?; | |
| fs::write(pkg_dir.path().join("unexpected"), "unexpected file".as_bytes())?; | |
| let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace( | |
| pkg_dir.path().to_str().unwrap(), | |
| fio::PERM_READABLE, | |
| )?; | |
| assert_matches!( | |
| pkg.verify_contents(&pkg_dir_proxy).await, | |
| Err(VerificationError::ExtraFile{ref path}) if path == "unexpected"); | |
| Ok(()) | |
| } | |
| #[fuchsia::test] | |
| async fn test_verify_contents_rejects_extra_meta_file() -> Result<(), Error> { | |
| let pkg = Package::identity().await?; | |
| let pkg_dir = make_this_package_dir()?; | |
| fs::write(pkg_dir.path().join("meta/unexpected"), "unexpected file".as_bytes())?; | |
| let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace( | |
| pkg_dir.path().to_str().unwrap(), | |
| fio::PERM_READABLE, | |
| )?; | |
| assert_matches!( | |
| pkg.verify_contents(&pkg_dir_proxy).await, | |
| Err(VerificationError::ExtraFile{ref path}) if path == "meta/unexpected"); | |
| Ok(()) | |
| } | |
| #[fuchsia::test] | |
| async fn test_verify_contents_rejects_missing_blob() -> Result<(), Error> { | |
| let pkg = Package::identity().await?; | |
| let pkg_dir = make_this_package_dir()?; | |
| fs::remove_file(pkg_dir.path().join("bin/fuchsia_pkg_testing_lib_test"))?; | |
| let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace( | |
| pkg_dir.path().to_str().unwrap(), | |
| fio::PERM_READABLE, | |
| )?; | |
| assert_matches!( | |
| pkg.verify_contents(&pkg_dir_proxy).await, | |
| Err(VerificationError::MissingFile{ref path}) if path == "bin/fuchsia_pkg_testing_lib_test"); | |
| Ok(()) | |
| } | |
| #[fuchsia::test] | |
| async fn test_verify_contents_rejects_different_contents() -> Result<(), Error> { | |
| let pkg = Package::identity().await?; | |
| let pkg_dir = make_this_package_dir()?; | |
| fs::write(pkg_dir.path().join("bin/fuchsia_pkg_testing_lib_test"), "broken".as_bytes())?; | |
| let pkg_dir_proxy = fuchsia_fs::directory::open_in_namespace( | |
| pkg_dir.path().to_str().unwrap(), | |
| fio::PERM_READABLE, | |
| )?; | |
| assert_matches!( | |
| pkg.verify_contents(&pkg_dir_proxy).await, | |
| Err(VerificationError::DifferentFileData{ref path}) if path == "bin/fuchsia_pkg_testing_lib_test"); | |
| Ok(()) | |
| } | |
| #[fuchsia::test] | |
| async fn test_meta_subpackages_with_no_subpackages() { | |
| let pkg = PackageBuilder::new("pkg").build().await.unwrap(); | |
| assert!(pkg.meta_subpackages().unwrap().subpackages().is_empty()); | |
| } | |
| #[fuchsia::test] | |
| async fn test_add_subpackage() { | |
| // Package with subpackage. | |
| let sub_sub_pkg = PackageBuilder::new("sub-sub-pkg") | |
| .add_resource_at("c-blob", "c-blob-contents".as_bytes()) | |
| .build() | |
| .await | |
| .unwrap(); | |
| let sub_pkg = PackageBuilder::new("sub-pkg") | |
| .add_resource_at("b-blob", "b-blob-contents".as_bytes()) | |
| .add_subpackage("subpackage-1", &sub_sub_pkg) | |
| .build() | |
| .await | |
| .unwrap(); | |
| let mut expected_subpackage_blobs = HashMap::new(); | |
| let (sub_sub_pkg_meta_far, content_blobs) = sub_sub_pkg.contents(); | |
| expected_subpackage_blobs | |
| .insert(sub_sub_pkg_meta_far.merkle, sub_sub_pkg_meta_far.contents); | |
| expected_subpackage_blobs | |
| .insert(content_blobs.into_keys().next().unwrap(), b"c-blob-contents".to_vec()); | |
| assert_eq!(*sub_pkg.subpackage_blobs().unwrap(), expected_subpackage_blobs); | |
| assert_eq!( | |
| sub_pkg.meta_subpackages().unwrap(), | |
| MetaSubpackages::from_iter([( | |
| fuchsia_url::RelativePackageUrl::parse("subpackage-1").unwrap(), | |
| sub_sub_pkg_meta_far.merkle | |
| )]) | |
| ); | |
| let (sub_pkg_meta_far, content_blobs) = sub_pkg.contents(); | |
| let mut expected_all_blobs = content_blobs | |
| .keys() | |
| .copied() | |
| .chain([sub_pkg_meta_far.merkle]) | |
| .chain(expected_subpackage_blobs.keys().copied()) | |
| .collect(); | |
| assert_eq!(sub_pkg.list_blobs(), expected_all_blobs); | |
| // Package with subpackage that is a superpackage. | |
| let pkg = PackageBuilder::new("pkg") | |
| .add_subpackage("subpackage-0", &sub_pkg) | |
| .build() | |
| .await | |
| .unwrap(); | |
| expected_subpackage_blobs.insert(sub_pkg_meta_far.merkle, sub_pkg_meta_far.contents); | |
| expected_subpackage_blobs | |
| .insert(content_blobs.into_keys().next().unwrap(), b"b-blob-contents".to_vec()); | |
| assert_eq!(*pkg.subpackage_blobs().unwrap(), expected_subpackage_blobs); | |
| assert_eq!( | |
| pkg.meta_subpackages().unwrap(), | |
| MetaSubpackages::from_iter([( | |
| fuchsia_url::RelativePackageUrl::parse("subpackage-0").unwrap(), | |
| sub_pkg_meta_far.merkle | |
| )]) | |
| ); | |
| expected_all_blobs.insert(*pkg.hash()); | |
| assert_eq!(pkg.list_blobs(), expected_all_blobs); | |
| } | |
| #[fuchsia::test] | |
| async fn test_add_subpackage_by_hash() { | |
| let pkg = PackageBuilder::new("pkg") | |
| .add_subpackage_by_hash("subpackage-name", Hash::from([0; 32])) | |
| .build() | |
| .await | |
| .unwrap(); | |
| assert_eq!(pkg.subpackage_blobs(), None); | |
| assert_eq!( | |
| pkg.meta_subpackages().unwrap(), | |
| MetaSubpackages::from_iter([( | |
| fuchsia_url::RelativePackageUrl::parse("subpackage-name").unwrap(), | |
| Hash::from([0; 32]) | |
| )]) | |
| ); | |
| } | |
| } |