blob: c585fde54a62f8d79fc5dc6cb1182e3934c73847 [file] [edit]
// 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])
)])
);
}
}