blob: 8edd0a770b8565429de17e32a24ab6fec6dd0e1e [file] [log] [blame]
// Copyright 2021 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.
//! Typesafe wrappers around an open package directory.
use crate::{
MetaContents, MetaContentsError, MetaPackage, MetaPackageError, MetaSubpackages,
MetaSubpackagesError,
};
use fidl::endpoints::ServerEnd;
use fidl_fuchsia_io as fio;
use fuchsia_hash::{Hash, ParseHashError};
use fuchsia_zircon_status as zx_status;
use thiserror::Error;
use version_history::AbiRevision;
// re-export wrapped fuchsia_fs errors.
pub use fuchsia_fs::{
file::ReadError,
node::{CloneError, CloseError, OpenError},
};
/// An error encountered while reading/parsing the package's hash
#[derive(Debug, Error)]
#[allow(missing_docs)]
pub enum ReadHashError {
#[error("while reading 'meta/'")]
Read(#[source] ReadError),
#[error("while parsing 'meta/'")]
Parse(#[source] ParseHashError),
}
/// An error encountered while reading/parsing the package's meta/package file
#[derive(Debug, Error)]
#[allow(missing_docs)]
pub enum LoadMetaPackageError {
#[error("while reading 'meta/package'")]
Read(#[source] ReadError),
#[error("while parsing 'meta/package'")]
Parse(#[source] MetaPackageError),
}
/// An error encountered while reading/parsing the package's
/// meta/fuchsia.pkg/subpackages file
#[derive(Debug, Error)]
#[allow(missing_docs)]
pub enum LoadMetaSubpackagesError {
#[error("while reading '{}'", MetaSubpackages::PATH)]
Read(#[source] ReadError),
#[error("while parsing '{}'", MetaSubpackages::PATH)]
Parse(#[source] MetaSubpackagesError),
}
/// An error encountered while reading/parsing the package's meta/contents file
#[derive(Debug, Error)]
#[allow(missing_docs)]
pub enum LoadMetaContentsError {
#[error("while reading 'meta/contents'")]
Read(#[source] ReadError),
#[error("while parsing 'meta/contents'")]
Parse(#[source] MetaContentsError),
}
/// An error encountered while reading/parsing the package's `AbiRevision`.
#[derive(Debug, Error)]
#[allow(missing_docs)]
pub enum LoadAbiRevisionError {
#[error("while opening '{}'", AbiRevision::PATH)]
Open(#[from] OpenError),
#[error("while reading '{}'", AbiRevision::PATH)]
Read(#[from] ReadError),
#[error("while parsing '{}'", AbiRevision::PATH)]
Parse(#[from] std::array::TryFromSliceError),
}
/// An open package directory
#[derive(Debug, Clone)]
pub struct PackageDirectory {
proxy: fio::DirectoryProxy,
}
impl PackageDirectory {
/// Interprets the provided directory proxy as a package dir.
pub fn from_proxy(proxy: fio::DirectoryProxy) -> Self {
Self { proxy }
}
/// Creates a new channel pair, returning the client end as Self and the
/// server end as a channel.
pub fn create_request() -> Result<(Self, ServerEnd<fio::DirectoryMarker>), fidl::Error> {
let (proxy, request) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>()?;
Ok((Self::from_proxy(proxy), request))
}
/// Returns the current component's package directory.
#[cfg(target_os = "fuchsia")]
pub fn open_from_namespace() -> Result<Self, OpenError> {
let dir = fuchsia_fs::directory::open_in_namespace("/pkg", fio::OpenFlags::RIGHT_READABLE)?;
Ok(Self::from_proxy(dir))
}
/// Cleanly close the package directory, consuming self.
pub async fn close(self) -> Result<(), CloseError> {
fuchsia_fs::directory::close(self.proxy).await
}
/// Send request to also serve this package directory on the given directory request.
pub fn reopen(&self, dir_request: ServerEnd<fio::DirectoryMarker>) -> Result<(), CloneError> {
fuchsia_fs::directory::clone_onto_no_describe(&self.proxy, None, dir_request)
}
/// Unwraps the inner DirectoryProxy, consuming self.
pub fn into_proxy(self) -> fio::DirectoryProxy {
self.proxy
}
/// Open the file in the package given by `path` with the given access `rights`.
pub async fn open_file(
&self,
path: &str,
rights: OpenRights,
) -> Result<fio::FileProxy, OpenError> {
fuchsia_fs::directory::open_file(&self.proxy, path, rights.to_flags()).await
}
/// Read the file in the package given by `path`, and return its contents as
/// a UTF-8 decoded string.
async fn read_file_to_string(&self, path: &str) -> Result<String, ReadError> {
let f = self.open_file(path, OpenRights::Read).await?;
fuchsia_fs::file::read_to_string(&f).await
}
/// Reads the merkle root of the package.
pub async fn merkle_root(&self) -> Result<Hash, ReadHashError> {
let merkle = self.read_file_to_string("meta").await.map_err(ReadHashError::Read)?;
merkle.parse().map_err(ReadHashError::Parse)
}
/// Reads and parses the package's meta/contents file.
pub async fn meta_contents(&self) -> Result<MetaContents, LoadMetaContentsError> {
let meta_contents =
self.read_file_to_string("meta/contents").await.map_err(LoadMetaContentsError::Read)?;
let meta_contents = MetaContents::deserialize(meta_contents.as_bytes())
.map_err(LoadMetaContentsError::Parse)?;
Ok(meta_contents)
}
/// Reads and parses the package's meta/package file.
pub async fn meta_package(&self) -> Result<MetaPackage, LoadMetaPackageError> {
let meta_package =
self.read_file_to_string("meta/package").await.map_err(LoadMetaPackageError::Read)?;
let meta_package = MetaPackage::deserialize(meta_package.as_bytes())
.map_err(LoadMetaPackageError::Parse)?;
Ok(meta_package)
}
/// Reads and parses the package's meta/fuchsia.pkg/subpackages file. If the file
/// doesn't exist, an empty `MetaSubpackages` is returned.
pub async fn meta_subpackages(&self) -> Result<MetaSubpackages, LoadMetaSubpackagesError> {
match self.read_file_to_string(MetaSubpackages::PATH).await {
Ok(file) => Ok(MetaSubpackages::deserialize(file.as_bytes())
.map_err(LoadMetaSubpackagesError::Parse)?),
Err(ReadError::Open(OpenError::OpenError(zx_status::Status::NOT_FOUND))) => {
Ok(MetaSubpackages::default())
}
Err(err) => Err(LoadMetaSubpackagesError::Read(err)),
}
}
/// Reads and parses the package's meta/fuchsia.abi/abi-revision file.
pub async fn abi_revision(&self) -> Result<AbiRevision, LoadAbiRevisionError> {
let abi_revision_bytes =
fuchsia_fs::file::read(&self.open_file(AbiRevision::PATH, OpenRights::Read).await?)
.await?;
Ok(AbiRevision::try_from(abi_revision_bytes.as_slice())?)
}
/// Returns an iterator of blobs needed by this package, does not include meta.far blob itself.
/// Hashes may appear more than once.
pub async fn blobs(&self) -> Result<impl Iterator<Item = Hash>, LoadMetaContentsError> {
Ok(self.meta_contents().await?.into_hashes_undeduplicated())
}
}
/// Possible open rights when opening a file within a package.
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum OpenRights {
Read,
ReadExecute,
}
impl OpenRights {
fn to_flags(&self) -> fio::OpenFlags {
match self {
OpenRights::Read => fio::OpenFlags::RIGHT_READABLE,
OpenRights::ReadExecute => {
fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE
}
}
}
}
#[cfg(test)]
#[cfg(target_os = "fuchsia")]
mod tests {
use super::*;
use assert_matches::assert_matches;
use fidl::endpoints::Proxy;
#[fuchsia_async::run_singlethreaded(test)]
async fn open_close() {
let pkg = PackageDirectory::open_from_namespace().unwrap();
let () = pkg.close().await.unwrap();
}
#[fuchsia_async::run_singlethreaded(test)]
async fn reopen_is_new_connection() {
let pkg = PackageDirectory::open_from_namespace().unwrap();
let (proxy, server_end) = fidl::endpoints::create_proxy().unwrap();
assert_matches!(pkg.reopen(server_end), Ok(()));
assert_matches!(PackageDirectory::from_proxy(proxy).close().await, Ok(()));
pkg.into_proxy().into_channel().expect("no other users of the wrapped channel");
}
#[fuchsia_async::run_singlethreaded(test)]
async fn merkle_root_is_pkg_meta() {
let pkg = PackageDirectory::open_from_namespace().unwrap();
let merkle: Hash = std::fs::read_to_string("/pkg/meta").unwrap().parse().unwrap();
assert_eq!(pkg.merkle_root().await.unwrap(), merkle);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn list_blobs() {
let pkg = PackageDirectory::open_from_namespace().unwrap();
// listing blobs succeeds, and this package has some blobs.
let blobs = pkg.blobs().await.unwrap().collect::<Vec<_>>();
assert!(!blobs.is_empty());
// the test duplicate blob appears twice
let duplicate_blob_merkle = fuchsia_merkle::from_slice("Hello World!".as_bytes()).root();
assert_eq!(blobs.iter().filter(|hash| *hash == &duplicate_blob_merkle).count(), 2);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn package_name_is_test_package_name() {
let pkg = PackageDirectory::open_from_namespace().unwrap();
assert_eq!(
pkg.meta_package().await.unwrap().into_path().to_string(),
"fuchsia-pkg-tests/0"
);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn missing_subpackages_file_is_empty_subpackages() {
let pkg = PackageDirectory::open_from_namespace().unwrap();
assert_eq!(pkg.meta_subpackages().await.unwrap(), MetaSubpackages::default(),);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn abi_revision_succeeds() {
let pkg = PackageDirectory::open_from_namespace().unwrap();
let _: AbiRevision = pkg.abi_revision().await.unwrap();
}
}