blob: d2fa451c8b0c04b78681ab4c36b2d9df79b2e08a [file] [log] [blame]
// 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>>;
#[derive(Error, Debug, Clone)]
pub enum ResolverRegistrationError {
#[error("a resolver is already registered with the URL scheme \"{}\"", 0)]
SchemeAlreadyRegistered(String),
}
/// 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>,
) -> Result<(), ResolverRegistrationError> {
if self.resolvers.contains_key(&scheme) {
Err(ResolverRegistrationError::SchemeAlreadyRegistered(scheme.clone()))
} else {
self.resolvers.insert(scheme, resolver);
Ok(())
}
}
/// Creates and populates a `ResolverRegistry` with `RemoteResolvers` that
/// have been registered with an environment.
pub fn from_decl(
decl: &[ResolverRegistration],
parent: &Arc<Realm>,
) -> Result<Self, ResolverRegistrationError> {
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(),
)),
)?;
}
Ok(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, matches::assert_matches};
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(),
}),
)
.unwrap();
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"))
}),
}),
)
.unwrap();
// 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]
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() };
assert_matches!(registry.register("fuchsia-pkg".to_string(), Box::new(resolver_a)), Ok(()));
assert_matches!(registry.register("fuchsia-pkg".to_string(), Box::new(resolver_b)), Err(ResolverRegistrationError::SchemeAlreadyRegistered(scheme)) if scheme == "fuchsia-pkg");
}
#[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)).unwrap();
registry.register("fuchsia-boot".to_string(), Box::new(resolver_b)).unwrap();
}
}