| // 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. |
| |
| //! Typesafe wrappers around the /pkgfs/versions filesystem. |
| |
| use { |
| fidl::{ |
| endpoints::{Proxy, RequestStream}, |
| AsHandleRef, |
| }, |
| fidl_fuchsia_io::{ |
| DirectoryMarker, DirectoryObject, DirectoryProxy, DirectoryRequest, DirectoryRequestStream, |
| NodeInfo, |
| }, |
| fuchsia_hash::Hash, |
| fuchsia_zircon::Status, |
| futures::prelude::*, |
| }; |
| |
| pub use crate::packages::OpenError; |
| |
| /// An open handle to /pkgfs/versions |
| #[derive(Debug, Clone)] |
| pub struct Client { |
| proxy: DirectoryProxy, |
| } |
| |
| impl Client { |
| /// Returns an client connected to pkgfs from the current component's namespace |
| pub fn open_from_namespace() -> Result<Self, io_util::node::OpenError> { |
| let proxy = io_util::directory::open_in_namespace( |
| "/pkgfs/versions", |
| fidl_fuchsia_io::OPEN_RIGHT_READABLE, |
| )?; |
| Ok(Client { proxy }) |
| } |
| |
| /// Returns an client connected to pkgfs from the given pkgfs root dir. |
| pub fn open_from_pkgfs_root(pkgfs: &DirectoryProxy) -> Result<Self, io_util::node::OpenError> { |
| Ok(Client { |
| proxy: io_util::directory::open_directory_no_describe( |
| pkgfs, |
| "versions", |
| fidl_fuchsia_io::OPEN_RIGHT_READABLE, |
| )?, |
| }) |
| } |
| |
| /// Creates a new client backed by the returned request stream. This constructor should not be |
| /// used outside of tests. |
| /// |
| /// # Panics |
| /// |
| /// Panics on error |
| pub fn new_test() -> (Self, DirectoryRequestStream) { |
| let (proxy, stream) = |
| fidl::endpoints::create_proxy_and_stream::<DirectoryMarker>().unwrap(); |
| |
| (Self { proxy }, stream) |
| } |
| |
| /// Creates a new client backed by the returned mock. This constructor should not be used |
| /// outside of tests. |
| /// |
| /// # Panics |
| /// |
| /// Panics on error |
| pub fn new_mock() -> (Self, Mock) { |
| let (proxy, stream) = |
| fidl::endpoints::create_proxy_and_stream::<DirectoryMarker>().unwrap(); |
| |
| (Self { proxy }, Mock { stream }) |
| } |
| |
| /// Open the package given by `meta_far_merkle`. Verifies the OnOpen event before returning. |
| pub async fn open_package( |
| &self, |
| meta_far_merkle: &Hash, |
| ) -> Result<fuchsia_pkg::PackageDirectory, OpenError> { |
| // TODO(fxbug.dev/37858) allow opening as executable too |
| let flags = fidl_fuchsia_io::OPEN_RIGHT_READABLE; |
| let dir = |
| io_util::directory::open_directory(&self.proxy, &meta_far_merkle.to_string(), flags) |
| .await |
| .map_err(|e| match e { |
| io_util::node::OpenError::OpenError(Status::NOT_FOUND) => OpenError::NotFound, |
| other => OpenError::Io(other), |
| })?; |
| |
| Ok(fuchsia_pkg::PackageDirectory::from_proxy(dir)) |
| } |
| } |
| |
| /// A testing server implementation of /pkgfs/versions. |
| /// |
| /// Mock does not handle requests until instructed to do so. |
| pub struct Mock { |
| stream: DirectoryRequestStream, |
| } |
| |
| impl Mock { |
| /// Consume the next directory request, verifying it is intended to open the package identified |
| /// by `merkle`. Returns a `MockPackage` representing the open package directory. |
| /// |
| /// # Panics |
| /// |
| /// Panics on error or assertion violation (unexpected requests or a mismatched open call) |
| pub async fn expect_open_package(&mut self, merkle: Hash) -> MockPackage { |
| match self.stream.next().await { |
| Some(Ok(DirectoryRequest::Open { |
| flags: _, |
| mode: _, |
| path, |
| object, |
| control_handle: _, |
| })) => { |
| assert_eq!(path, merkle.to_string()); |
| |
| let stream = object.into_stream().unwrap().cast_stream(); |
| MockPackage { stream } |
| } |
| other => panic!("unexpected request: {:?}", other), |
| } |
| } |
| |
| /// Asserts that the request stream closes without any further requests. |
| /// |
| /// # Panics |
| /// |
| /// Panics on error |
| pub async fn expect_done(mut self) { |
| match self.stream.next().await { |
| None => {} |
| Some(request) => panic!("unexpected request: {:?}", request), |
| } |
| } |
| } |
| |
| /// A testing server implementation of an open /pkgfs/versions/<merkle> directory. |
| /// |
| /// MockBlob does not send the OnOpen event or handle requests until instructed to do so. |
| pub struct MockPackage { |
| stream: DirectoryRequestStream, |
| } |
| |
| impl MockPackage { |
| fn send_on_open(&mut self, status: Status) { |
| let mut info = NodeInfo::Directory(DirectoryObject); |
| let () = |
| self.stream.control_handle().send_on_open_(status.into_raw(), Some(&mut info)).unwrap(); |
| } |
| |
| /// Fail the open request with an error indicating the package does not exist. |
| /// |
| /// # Panics |
| /// |
| /// Panics on error |
| pub async fn fail_open_with_not_found(mut self) { |
| self.send_on_open(Status::NOT_FOUND); |
| } |
| |
| /// Succeeds the open request. |
| /// |
| /// # Panics |
| /// |
| /// Panics on error |
| pub async fn succeed_open(mut self) -> Self { |
| self.send_on_open(Status::OK); |
| self |
| } |
| |
| /// Consume the next directory request, verifying it is a request to duplicate this package |
| /// onto another handle. Returns a `MockPackage` representing the duplicated package |
| /// directory. |
| /// |
| /// # Panics |
| /// |
| /// Panics on error or assertion violation (unexpected requests or a mismatched clone call) |
| pub async fn expect_clone(&mut self) -> MockPackage { |
| match self.stream.next().await { |
| Some(Ok(DirectoryRequest::Clone { flags: _, object, control_handle: _ })) => { |
| let stream = object.into_stream().unwrap().cast_stream(); |
| MockPackage { stream } |
| } |
| other => panic!("unexpected request: {:?}", other), |
| } |
| } |
| |
| /// Verify this directory's channel is the server end for the given proxy dir, consuming both |
| /// objects. |
| /// |
| /// # Panics |
| /// |
| /// Panics on error or if the objects are not using the same channel. |
| pub fn verify_are_same_channel(self, proxy: DirectoryProxy) { |
| let expected_server_end_koid = proxy.as_channel().basic_info().unwrap().related_koid; |
| let actual_server_end_koid = self.stream.into_inner().0.channel().get_koid().unwrap(); |
| |
| assert_eq!(expected_server_end_koid, actual_server_end_koid); |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, crate::install::BlobKind, fuchsia_pkg_testing::PackageBuilder, |
| matches::assert_matches, pkgfs_ramdisk::PkgfsRamdisk, |
| }; |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn open_non_existant_package_fails() { |
| let pkgfs = PkgfsRamdisk::start().unwrap(); |
| let root = pkgfs.root_dir_proxy().unwrap(); |
| let client = Client::open_from_pkgfs_root(&root).unwrap(); |
| |
| let merkle = fuchsia_merkle::MerkleTree::from_reader(std::io::empty()).unwrap().root(); |
| assert_matches!(client.open_package(&merkle).await, Err(OpenError::NotFound)); |
| |
| pkgfs.stop().await.unwrap(); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn open_package_single_blob() { |
| let pkgfs = PkgfsRamdisk::start().unwrap(); |
| let root = pkgfs.root_dir_proxy().unwrap(); |
| let install = crate::install::Client::open_from_pkgfs_root(&root).unwrap(); |
| let client = Client::open_from_pkgfs_root(&root).unwrap(); |
| |
| let pkg = PackageBuilder::new("uniblob").build().await.unwrap(); |
| let pkg_merkle = pkg.meta_far_merkle_root().to_owned(); |
| install.write_meta_far(&pkg).await; |
| |
| assert_matches!(client.open_package(&pkg_merkle).await, Ok(_)); |
| assert_matches!( |
| pkg.verify_contents(&client.open_package(&pkg_merkle).await.unwrap().into_proxy()) |
| .await, |
| Ok(()) |
| ); |
| |
| pkgfs.stop().await.unwrap(); |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn open_package_multiple_blobs() { |
| let pkgfs = PkgfsRamdisk::start().unwrap(); |
| let root = pkgfs.root_dir_proxy().unwrap(); |
| let install = crate::install::Client::open_from_pkgfs_root(&root).unwrap(); |
| let client = Client::open_from_pkgfs_root(&root).unwrap(); |
| |
| let pkg = PackageBuilder::new("multiblob") |
| .add_resource_at("data/first", "contents of first blob".as_bytes()) |
| .add_resource_at("data/second", "contents of second blob".as_bytes()) |
| .build() |
| .await |
| .unwrap(); |
| let pkg_contents = pkg.meta_contents().unwrap().contents().to_owned(); |
| let pkg_merkle = pkg.meta_far_merkle_root().to_owned(); |
| install.write_meta_far(&pkg).await; |
| |
| // Package is not complete yet, so opening fails. |
| assert_matches!(client.open_package(&pkg_merkle).await, Err(OpenError::NotFound)); |
| |
| install |
| .write_blob( |
| pkg_contents["data/first"], |
| BlobKind::Data, |
| "contents of first blob".as_bytes(), |
| ) |
| .await; |
| install |
| .write_blob( |
| pkg_contents["data/second"], |
| BlobKind::Data, |
| "contents of second blob".as_bytes(), |
| ) |
| .await; |
| |
| assert_matches!(client.open_package(&pkg_merkle).await, Ok(_)); |
| assert_matches!( |
| pkg.verify_contents(&client.open_package(&pkg_merkle).await.unwrap().into_proxy()) |
| .await, |
| Ok(()) |
| ); |
| |
| pkgfs.stop().await.unwrap(); |
| } |
| } |