| // 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. |
| |
| use { |
| crate::{ |
| capability::EnvironmentCapability, |
| model::{ |
| error::ModelError, |
| realm::{Realm, WeakRealm}, |
| routing, |
| }, |
| }, |
| anyhow::Error, |
| clonable_error::ClonableError, |
| cm_rust::{CapabilityName, RegistrationSource, ResolverRegistration}, |
| fidl_fuchsia_io as fio, fidl_fuchsia_sys2 as fsys, |
| fuchsia_zircon::Status, |
| futures::future::{self, BoxFuture}, |
| std::{collections::HashMap, sync::Arc}, |
| thiserror::Error, |
| url::Url, |
| }; |
| |
| /// Resolves a component URL to its content. |
| /// TODO: Consider defining an internal representation for `fsys::Component` so as to |
| /// further isolate the `Model` from FIDL interfacting concerns. |
| pub trait Resolver { |
| fn resolve<'a>(&'a self, component_url: &'a str) -> ResolverFut<'a>; |
| } |
| |
| pub type ResolverFut<'a> = BoxFuture<'a, Result<fsys::Component, ResolverError>>; |
| |
| /// Resolves a component URL using a resolver selected based on the URL's scheme. |
| #[derive(Default)] |
| pub struct ResolverRegistry { |
| resolvers: HashMap<String, Box<dyn Resolver + Send + Sync + 'static>>, |
| } |
| |
| impl ResolverRegistry { |
| pub fn new() -> ResolverRegistry { |
| Default::default() |
| } |
| |
| pub fn register( |
| &mut self, |
| scheme: String, |
| resolver: Box<dyn Resolver + Send + Sync + 'static>, |
| ) { |
| // ComponentDecl validation checks that there aren't any duplicate schemes. |
| assert!( |
| self.resolvers.insert(scheme, resolver).is_none(), |
| "Found duplicate scheme in ComponentDecl" |
| ); |
| } |
| |
| /// Creates and populates a `ResolverRegistry` with `RemoteResolvers` that |
| /// have been registered with an environment. |
| pub fn from_decl(decl: &[ResolverRegistration], parent: &Arc<Realm>) -> Self { |
| let mut registry = ResolverRegistry::new(); |
| for resolver in decl { |
| registry.register( |
| resolver.scheme.clone().into(), |
| Box::new(RemoteResolver::new( |
| resolver.resolver.clone(), |
| resolver.source.clone(), |
| parent.as_weak(), |
| )), |
| ); |
| } |
| registry |
| } |
| } |
| |
| impl Resolver for ResolverRegistry { |
| fn resolve<'a>(&'a self, component_url: &'a str) -> ResolverFut<'a> { |
| match Url::parse(component_url) { |
| Ok(parsed_url) => { |
| if let Some(ref resolver) = self.resolvers.get(parsed_url.scheme()) { |
| resolver.resolve(component_url) |
| } else { |
| Box::pin(future::err(ResolverError::SchemeNotRegistered)) |
| } |
| } |
| Err(e) => Box::pin(future::err(ResolverError::url_parse_error(component_url, e))), |
| } |
| } |
| } |
| |
| /// A resolver whose implementation lives in an external component. The source |
| /// of the resolver is determined through capability routing. |
| pub struct RemoteResolver { |
| capability_name: CapabilityName, |
| source: RegistrationSource, |
| realm: WeakRealm, |
| } |
| |
| impl RemoteResolver { |
| pub fn new(name: CapabilityName, source: RegistrationSource, realm: WeakRealm) -> Self { |
| RemoteResolver { capability_name: name, source, realm } |
| } |
| } |
| |
| // TODO(61288): Implement some sort of caching of the routed capability. Multiple |
| // component URL resolutions should be possible on a single channel. |
| impl Resolver for RemoteResolver { |
| fn resolve<'a>(&'a self, component_url: &'a str) -> ResolverFut<'a> { |
| Box::pin(async move { |
| let decl = EnvironmentCapability::Resolver { |
| source_name: self.capability_name.clone(), |
| source: self.source.clone(), |
| }; |
| let flags = fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_WRITABLE; |
| let open_mode = fio::MODE_TYPE_SERVICE; |
| let (proxy, server_end) = |
| fidl::endpoints::create_proxy::<fsys::ComponentResolverMarker>() |
| .map_err(ResolverError::unknown_resolver_error)?; |
| let realm = self.realm.upgrade().map_err(ResolverError::routing_error)?; |
| routing::route_capability_from_environment( |
| flags, |
| open_mode, |
| String::new(), |
| decl, |
| &realm, |
| &mut server_end.into_channel(), |
| ) |
| .await |
| .map_err(ResolverError::routing_error)?; |
| let (status, component) = proxy |
| .resolve(component_url) |
| .await |
| .map_err(ResolverError::unknown_resolver_error)?; |
| let status = Status::from_raw(status); |
| match status { |
| Status::OK => { |
| let decl = component.decl.as_ref().ok_or(ResolverError::RemoteInvalidData)?; |
| cm_fidl_validator::validate(decl) |
| .map_err(|e| ResolverError::manifest_invalid(component_url, e))?; |
| Ok(component) |
| } |
| Status::INVALID_ARGS => { |
| Err(ResolverError::url_parse_error(component_url, RemoteError(status))) |
| } |
| Status::NOT_FOUND => { |
| Err(ResolverError::component_not_available(component_url, RemoteError(status))) |
| } |
| Status::UNAVAILABLE => { |
| Err(ResolverError::manifest_invalid(component_url, RemoteError(status))) |
| } |
| _ => Err(ResolverError::unknown_resolver_error(RemoteError(status))), |
| } |
| }) |
| } |
| } |
| |
| /// Errors produced by `Resolver`. |
| #[derive(Debug, Error, Clone)] |
| pub enum ResolverError { |
| #[error("resolver is already registered for scheme \"{}\"", scheme)] |
| DuplicateResolverError { scheme: String }, |
| #[error("component not available with url \"{}\": {}", url, err)] |
| ComponentNotAvailable { |
| url: String, |
| #[source] |
| err: ClonableError, |
| }, |
| #[error("component manifest not available for url \"{}\": {}", url, err)] |
| ManifestNotAvailable { |
| url: String, |
| #[source] |
| err: ClonableError, |
| }, |
| #[error("component manifest invalid for url \"{}\": {}", url, err)] |
| ManifestInvalid { |
| url: String, |
| #[source] |
| err: ClonableError, |
| }, |
| #[error("Model not available.")] |
| ModelAccessError, |
| #[error("scheme not registered")] |
| SchemeNotRegistered, |
| #[error("failed to parse url \"{}\": {}", url, err)] |
| UrlParseError { |
| url: String, |
| #[source] |
| err: ClonableError, |
| }, |
| #[error("url missing resource \"{}\"", url)] |
| UrlMissingResourceError { url: String }, |
| #[error("failed to route resolver capability: {}", .0)] |
| RoutingError(#[source] Box<ModelError>), |
| #[error("the remote resolver returned invalid data")] |
| RemoteInvalidData, |
| #[error("an unknown error ocurred with the resolver: {}", .0)] |
| UnknownResolverError(#[source] ClonableError), |
| } |
| |
| impl ResolverError { |
| pub fn component_not_available(url: impl Into<String>, err: impl Into<Error>) -> ResolverError { |
| ResolverError::ComponentNotAvailable { url: url.into(), err: err.into().into() } |
| } |
| |
| pub fn manifest_not_available(url: impl Into<String>, err: impl Into<Error>) -> ResolverError { |
| ResolverError::ManifestNotAvailable { url: url.into(), err: err.into().into() } |
| } |
| |
| pub fn manifest_invalid(url: impl Into<String>, err: impl Into<Error>) -> ResolverError { |
| ResolverError::ManifestInvalid { url: url.into(), err: err.into().into() } |
| } |
| |
| pub fn model_not_available() -> ResolverError { |
| ResolverError::ModelAccessError |
| } |
| |
| pub fn url_parse_error(url: impl Into<String>, err: impl Into<Error>) -> ResolverError { |
| ResolverError::UrlParseError { url: url.into(), err: err.into().into() } |
| } |
| |
| pub fn url_missing_resource_error(url: impl Into<String>) -> ResolverError { |
| ResolverError::UrlMissingResourceError { url: url.into() } |
| } |
| |
| pub fn routing_error(err: ModelError) -> ResolverError { |
| ResolverError::RoutingError(Box::new(err)) |
| } |
| |
| pub fn unknown_resolver_error(err: impl Into<Error>) -> ResolverError { |
| ResolverError::UnknownResolverError(err.into().into()) |
| } |
| } |
| |
| #[derive(Error, Clone, Debug)] |
| #[error("remote resolver returned status {}", .0)] |
| struct RemoteError(Status); |
| |
| #[cfg(test)] |
| mod tests { |
| use {super::*, anyhow::format_err}; |
| |
| struct MockOkResolver { |
| pub expected_url: String, |
| pub resolved_url: String, |
| } |
| |
| impl Resolver for MockOkResolver { |
| fn resolve<'a>(&'a self, component_url: &'a str) -> ResolverFut<'a> { |
| assert_eq!(self.expected_url, component_url); |
| Box::pin(future::ok(fsys::Component { |
| resolved_url: Some(self.resolved_url.clone()), |
| decl: Some(fsys::ComponentDecl { |
| program: None, |
| uses: None, |
| exposes: None, |
| offers: None, |
| facets: None, |
| capabilities: None, |
| children: None, |
| collections: None, |
| environments: None, |
| ..fsys::ComponentDecl::EMPTY |
| }), |
| package: None, |
| ..fsys::Component::EMPTY |
| })) |
| } |
| } |
| |
| struct MockErrorResolver { |
| pub expected_url: String, |
| pub error: Box<dyn Fn(&str) -> ResolverError + Send + Sync + 'static>, |
| } |
| |
| impl Resolver for MockErrorResolver { |
| fn resolve<'a>(&'a self, component_url: &'a str) -> ResolverFut<'a> { |
| assert_eq!(self.expected_url, component_url); |
| Box::pin(future::err((self.error)(component_url))) |
| } |
| } |
| |
| #[fuchsia_async::run_until_stalled(test)] |
| async fn register_and_resolve() { |
| let mut registry = ResolverRegistry::new(); |
| registry.register( |
| "foo".to_string(), |
| Box::new(MockOkResolver { |
| expected_url: "foo://url".to_string(), |
| resolved_url: "foo://resolved".to_string(), |
| }), |
| ); |
| registry.register( |
| "bar".to_string(), |
| Box::new(MockErrorResolver { |
| expected_url: "bar://url".to_string(), |
| error: Box::new(|url| { |
| ResolverError::component_not_available(url, format_err!("not available")) |
| }), |
| }), |
| ); |
| |
| // Resolve known scheme that returns success. |
| let component = registry.resolve("foo://url").await.unwrap(); |
| assert_eq!("foo://resolved", component.resolved_url.unwrap()); |
| |
| // Resolve a different scheme that produces an error. |
| let expected_res: Result<fsys::Component, ResolverError> = |
| Err(ResolverError::component_not_available("bar://url", format_err!("not available"))); |
| assert_eq!( |
| format!("{:?}", expected_res), |
| format!("{:?}", registry.resolve("bar://url").await) |
| ); |
| |
| // Resolve an unknown scheme |
| let expected_res: Result<fsys::Component, ResolverError> = |
| Err(ResolverError::SchemeNotRegistered); |
| assert_eq!( |
| format!("{:?}", expected_res), |
| format!("{:?}", registry.resolve("unknown://url").await), |
| ); |
| |
| // Resolve an URL lacking a scheme. |
| let expected_res: Result<fsys::Component, ResolverError> = |
| Err(ResolverError::url_parse_error("xxx", url::ParseError::RelativeUrlWithoutBase)); |
| assert_eq!(format!("{:?}", expected_res), format!("{:?}", registry.resolve("xxx").await),); |
| } |
| |
| #[test] |
| #[should_panic(expected = "Found duplicate scheme in ComponentDecl")] |
| fn test_duplicate_registration() { |
| let mut registry = ResolverRegistry::new(); |
| let resolver_a = |
| MockOkResolver { expected_url: "".to_string(), resolved_url: "".to_string() }; |
| let resolver_b = |
| MockOkResolver { expected_url: "".to_string(), resolved_url: "".to_string() }; |
| registry.register("fuchsia-pkg".to_string(), Box::new(resolver_a)); |
| registry.register("fuchsia-pkg".to_string(), Box::new(resolver_b)); |
| } |
| |
| #[test] |
| fn test_multiple_scheme_registration() { |
| let mut registry = ResolverRegistry::new(); |
| let resolver_a = |
| MockOkResolver { expected_url: "".to_string(), resolved_url: "".to_string() }; |
| let resolver_b = |
| MockOkResolver { expected_url: "".to_string(), resolved_url: "".to_string() }; |
| registry.register("fuchsia-pkg".to_string(), Box::new(resolver_a)); |
| registry.register("fuchsia-boot".to_string(), Box::new(resolver_b)); |
| } |
| } |