| // Copyright 2020 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. |
| |
| use { |
| anyhow::{self, Context}, |
| fidl::endpoints::{ClientEnd, Proxy}, |
| fidl_fuchsia_io::{self as fio, DirectoryProxy}, |
| fidl_fuchsia_sys2::{self as fsys, ComponentResolverRequest, ComponentResolverRequestStream}, |
| fuchsia_async as fasync, |
| fuchsia_component::server::ServiceFs, |
| fuchsia_url::{errors::ParseError as PkgUrlParseError, pkg_url::PkgUrl}, |
| fuchsia_zircon::Status, |
| futures::prelude::*, |
| log::*, |
| thiserror::Error, |
| }; |
| |
| /// Wraps all hosted protocols into a single type that can be matched against |
| /// and dispatched. |
| enum IncomingRequest { |
| ComponentResolver(ComponentResolverRequestStream), |
| } |
| |
| #[fasync::run_singlethreaded] |
| async fn main() -> Result<(), anyhow::Error> { |
| fuchsia_syslog::init().expect("failed to initialize logging"); |
| let mut service_fs = ServiceFs::new_local(); |
| service_fs.dir("svc").add_fidl_service(IncomingRequest::ComponentResolver); |
| service_fs.take_and_serve_directory_handle().context("failed to serve outgoing namespace")?; |
| service_fs |
| .for_each_concurrent(None, |IncomingRequest::ComponentResolver(stream)| async move { |
| match serve(stream).await { |
| Ok(()) => {} |
| Err(err) => error!("failed to serve resolve request: {:?}", err), |
| } |
| }) |
| .await; |
| |
| Ok(()) |
| } |
| |
| async fn serve(mut stream: ComponentResolverRequestStream) -> Result<(), anyhow::Error> { |
| let pkgfs_dir = io_util::open_directory_in_namespace( |
| "/pkgfs", |
| fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_EXECUTABLE, |
| ) |
| .context("failed to open /pkgfs")?; |
| while let Some(ComponentResolverRequest::Resolve { component_url, responder }) = |
| stream.try_next().await.context("failed to read request from FIDL stream")? |
| { |
| match resolve_component(&component_url, &pkgfs_dir).await { |
| Ok(result) => responder.send(Status::OK.into_raw(), result), |
| Err(err) => { |
| error!("failed to resolve component URL {}: {}", &component_url, &err); |
| responder.send(err.as_zx_status(), fsys::Component::EMPTY) |
| } |
| } |
| .context("failed sending response")?; |
| } |
| Ok(()) |
| } |
| |
| async fn resolve_component( |
| component_url: &str, |
| pkgfs_dir: &DirectoryProxy, |
| ) -> Result<fsys::Component, ResolverError> { |
| let package_url = PkgUrl::parse(component_url)?; |
| let cm_path = package_url |
| .resource() |
| .ok_or_else(|| ResolverError::InvalidUrl(PkgUrlParseError::InvalidResourcePath))?; |
| let package_dir = resolve_package(&package_url, pkgfs_dir).await?; |
| |
| // Read the component manifest (.cm file) from the package directory. |
| let cm_file = io_util::directory::open_file(&package_dir, cm_path, fio::OPEN_RIGHT_READABLE) |
| .await |
| .map_err(ResolverError::ComponentNotFound)?; |
| let component_decl = |
| io_util::file::read_fidl(&cm_file).await.map_err(ResolverError::InvalidManifest)?; |
| |
| let package_dir = ClientEnd::new( |
| package_dir.into_channel().expect("could not convert proxy to channel").into_zx_channel(), |
| ); |
| Ok(fsys::Component { |
| resolved_url: Some(component_url.into()), |
| decl: Some(component_decl), |
| package: Some(fsys::Package { |
| package_url: Some(package_url.root_url().to_string()), |
| package_dir: Some(package_dir), |
| ..fsys::Package::EMPTY |
| }), |
| ..fsys::Component::EMPTY |
| }) |
| } |
| |
| async fn resolve_package( |
| package_url: &PkgUrl, |
| pkgfs_dir: &DirectoryProxy, |
| ) -> Result<DirectoryProxy, ResolverError> { |
| let root_url = package_url.root_url(); |
| if root_url.host() != "fuchsia.com" { |
| return Err(ResolverError::UnsupportedRepo); |
| } |
| let package_name = io_util::canonicalize_path(root_url.path()); |
| // Package contents are available at `packages/$PACKAGE_NAME/0`. |
| let dir = io_util::directory::open_directory( |
| pkgfs_dir, |
| &format!("packages/{}/0", package_name), |
| fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_EXECUTABLE, |
| ) |
| .await |
| .map_err(ResolverError::PackageNotFound)?; |
| Ok(dir) |
| } |
| |
| #[derive(Error, Debug)] |
| enum ResolverError { |
| #[error("invalid component URL: {}", .0)] |
| InvalidUrl(#[from] PkgUrlParseError), |
| #[error("the hostname refers to an unsupported repo")] |
| UnsupportedRepo, |
| #[error("invalid component manifest: {}", .0)] |
| InvalidManifest(#[source] anyhow::Error), |
| #[error("component not found: {}", .0)] |
| ComponentNotFound(#[source] io_util::node::OpenError), |
| #[error("package not found: {}", .0)] |
| PackageNotFound(#[source] io_util::node::OpenError), |
| } |
| |
| impl ResolverError { |
| fn as_zx_status(&self) -> i32 { |
| match self { |
| Self::InvalidUrl(_) => Status::INVALID_ARGS.into_raw(), |
| Self::UnsupportedRepo => Status::NOT_SUPPORTED.into_raw(), |
| Self::InvalidManifest(_) => Status::IO_INVALID.into_raw(), |
| Self::ComponentNotFound(_) => Status::NOT_FOUND.into_raw(), |
| Self::PackageNotFound(_) => Status::NOT_FOUND.into_raw(), |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| fake_pkgfs::{Entry, MockDir, MockFile}, |
| fidl::encoding::encode_persistent, |
| fidl::endpoints::{create_proxy, RequestStream, ServerEnd}, |
| fidl_fuchsia_io::{DirectoryMarker, DirectoryObject, NodeInfo, NodeMarker}, |
| matches::assert_matches, |
| std::sync::Arc, |
| }; |
| |
| /// A DirectoryEntry implementation that checks whether an expected set of flags |
| /// are set in the Open request. |
| struct FlagVerifier(u32); |
| |
| impl Entry for FlagVerifier { |
| fn open( |
| self: Arc<Self>, |
| flags: u32, |
| _mode: u32, |
| _path: &str, |
| server_end: ServerEnd<NodeMarker>, |
| ) { |
| let status = if flags & self.0 != self.0 { Status::INVALID_ARGS } else { Status::OK }; |
| let stream = server_end.into_stream().expect("failed to create stream"); |
| let control_handle = stream.control_handle(); |
| control_handle |
| .send_on_open_( |
| status.into_raw(), |
| Some(&mut NodeInfo::Directory(DirectoryObject {})), |
| ) |
| .expect("failed to send OnOpen event"); |
| control_handle.shutdown_with_epitaph(status); |
| } |
| } |
| |
| fn serve_pkgfs(pseudo_dir: Arc<dyn Entry>) -> Result<DirectoryProxy, anyhow::Error> { |
| let (proxy, server_end) = create_proxy::<DirectoryMarker>() |
| .context("failed to create DirectoryProxy/Server pair")?; |
| pseudo_dir.open( |
| fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_EXECUTABLE, |
| fio::MODE_TYPE_DIRECTORY, |
| ".", |
| ServerEnd::new(server_end.into_channel()), |
| ); |
| Ok(proxy) |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn resolves_package_with_executable_rights() { |
| let pkg_url = PkgUrl::new_package("fuchsia.com".into(), "/test-package".into(), None) |
| .expect("failed to create test PkgUrl"); |
| let flag_verifier = |
| Arc::new(FlagVerifier(fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_EXECUTABLE)); |
| let pkgfs_dir = |
| serve_pkgfs(Arc::new(MockDir::new().add_entry( |
| "packages", |
| Arc::new(MockDir::new().add_entry( |
| "test-package", |
| Arc::new(MockDir::new().add_entry("0", flag_verifier)), |
| )), |
| ))) |
| .expect("failed to serve pkgfs"); |
| |
| let package = |
| resolve_package(&pkg_url, &pkgfs_dir).await.expect("failed to resolve package"); |
| let event_stream = package.take_event_stream().map_ok(|_| ()); |
| assert_matches!( |
| event_stream.try_collect::<()>().await, |
| Err(fidl::Error::ClientChannelClosed { status: Status::OK, .. }) |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn fails_to_resolve_package_unsupported_repo() { |
| let pkg_url = PkgUrl::new_package("fuchsia.ca".into(), "/test-package".into(), None) |
| .expect("failed to create test PkgUrl"); |
| let pkgfs_dir = serve_pkgfs(Arc::new(MockDir::new())).expect("failed to serve pkgfs"); |
| assert_matches!( |
| resolve_package(&pkg_url, &pkgfs_dir).await, |
| Err(ResolverError::UnsupportedRepo) |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn fails_to_resolve_component_invalid_url() { |
| let pkgfs_dir = serve_pkgfs(Arc::new(MockDir::new())).expect("failed to serve pkgfs"); |
| assert_matches!( |
| resolve_component("fuchsia://fuchsia.com/foo#meta/bar.cm", &pkgfs_dir).await, |
| Err(ResolverError::InvalidUrl(_)) |
| ); |
| assert_matches!( |
| resolve_component("fuchsia-pkg://fuchsia.com/foo", &pkgfs_dir).await, |
| Err(ResolverError::InvalidUrl(_)) |
| ); |
| assert_matches!( |
| resolve_component("fuchsia-pkg://fuchsia.com/#meta/bar.cm", &pkgfs_dir).await, |
| Err(ResolverError::InvalidUrl(_)) |
| ); |
| assert_matches!( |
| resolve_component("fuchsia-pkg://fuchsia.ca/foo#meta/bar.cm", &pkgfs_dir).await, |
| Err(ResolverError::UnsupportedRepo) |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn fails_to_resolve_component_package_not_found() { |
| let pkgfs_dir = serve_pkgfs(build_fake_pkgfs()).expect("failed to serve pkgfs"); |
| assert_matches!( |
| resolve_component("fuchsia-pkg://fuchsia.com/missing-package#meta/foo.cm", &pkgfs_dir) |
| .await, |
| Err(ResolverError::PackageNotFound(_)) |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn fails_to_resolve_component_missing_manifest() { |
| let pkgfs_dir = serve_pkgfs(build_fake_pkgfs()).expect("failed to serve pkgfs"); |
| assert_matches!( |
| resolve_component("fuchsia-pkg://fuchsia.com/test-package#meta/bar.cm", &pkgfs_dir) |
| .await, |
| Err(ResolverError::ComponentNotFound(_)) |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn fails_to_resolve_component_invalid_manifest() { |
| let pkgfs_dir = serve_pkgfs(build_fake_pkgfs()).expect("failed to serve pkgfs"); |
| assert_matches!( |
| resolve_component("fuchsia-pkg://fuchsia.com/test-package#meta/bad.cm", &pkgfs_dir) |
| .await, |
| Err(ResolverError::InvalidManifest(_)) |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn resolves_component_successfully() { |
| fuchsia_syslog::init().unwrap(); |
| fuchsia_syslog::set_severity(fuchsia_syslog::levels::INFO); |
| let pkgfs_dir = serve_pkgfs(build_fake_pkgfs()).expect("failed to serve pkgfs"); |
| assert_matches!( |
| resolve_component("fuchsia-pkg://fuchsia.com/test-package#meta/foo.cm", &pkgfs_dir) |
| .await, |
| Ok(_) |
| ); |
| } |
| |
| fn build_fake_pkgfs() -> Arc<MockDir> { |
| let cm_bytes = encode_persistent(&mut fsys::ComponentDecl::EMPTY.clone()) |
| .expect("failed to encode ComponentDecl FIDL"); |
| Arc::new( |
| MockDir::new().add_entry( |
| "packages", |
| Arc::new( |
| MockDir::new().add_entry( |
| "test-package", |
| Arc::new( |
| MockDir::new().add_entry( |
| "0", |
| Arc::new( |
| MockDir::new().add_entry( |
| "meta", |
| Arc::new( |
| MockDir::new() |
| .add_entry( |
| "foo.cm", |
| Arc::new(MockFile::new(cm_bytes)), |
| ) |
| .add_entry( |
| "bad.cm", |
| Arc::new(MockFile::new(b"foo".to_vec())), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ) |
| } |
| } |