blob: 5837c60f269b10baebec7528f8bef880d28c346b [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::model::{
component::{ComponentInstance, WeakComponentInstance},
error::ModelError,
routing::{route_and_open_capability, OpenOptions, OpenResolverOptions, RouteRequest},
},
::routing::component_instance::ComponentInstanceInterface,
anyhow::Error,
async_trait::async_trait,
clonable_error::ClonableError,
cm_rust::ResolverRegistration,
fidl_fuchsia_io as fio, fidl_fuchsia_mem as fmem, fidl_fuchsia_sys2 as fsys,
fuchsia_zircon::Status,
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.
#[async_trait]
pub trait Resolver {
async fn resolve(&self, component_url: &str) -> Result<ResolvedComponent, ResolverError>;
}
/// The response returned from a Resolver. This struct is derived from the FIDL
/// [`fuchsia.sys2.Component`][fidl_fuchsia_sys2::Component] table, except that
/// the opaque binary ComponentDecl has been deserialized and validated.
#[derive(Debug)]
pub struct ResolvedComponent {
pub resolved_url: String,
pub decl: fsys::ComponentDecl,
pub package: Option<fsys::Package>,
}
/// 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<ComponentInstance>) -> Self {
let mut registry = ResolverRegistry::new();
for resolver in decl {
registry.register(
resolver.scheme.clone().into(),
Box::new(RemoteResolver::new(resolver.clone(), parent.as_weak())),
);
}
registry
}
}
#[async_trait]
impl Resolver for ResolverRegistry {
async fn resolve(&self, component_url: &str) -> Result<ResolvedComponent, ResolverError> {
match Url::parse(component_url) {
Ok(parsed_url) => {
if let Some(ref resolver) = self.resolvers.get(parsed_url.scheme()) {
resolver.resolve(component_url).await
} else {
Err(ResolverError::SchemeNotRegistered)
}
}
Err(e) => Err(ResolverError::malformed_url(e)),
}
}
}
/// A resolver whose implementation lives in an external component. The source
/// of the resolver is determined through capability routing.
pub struct RemoteResolver {
registration: ResolverRegistration,
component: WeakComponentInstance,
}
impl RemoteResolver {
pub fn new(registration: ResolverRegistration, component: WeakComponentInstance) -> Self {
RemoteResolver { registration, component }
}
}
// TODO(61288): Implement some sort of caching of the routed capability. Multiple
// component URL resolutions should be possible on a single channel.
#[async_trait]
impl Resolver for RemoteResolver {
async fn resolve(&self, component_url: &str) -> Result<ResolvedComponent, ResolverError> {
let (proxy, server_end) = fidl::endpoints::create_proxy::<fsys::ComponentResolverMarker>()
.map_err(ResolverError::internal)?;
let component = self.component.upgrade().map_err(ResolverError::routing_error)?;
let open_options = OpenResolverOptions {
flags: fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_WRITABLE,
open_mode: fio::MODE_TYPE_SERVICE,
server_chan: &mut server_end.into_channel(),
};
route_and_open_capability(
RouteRequest::Resolver(self.registration.clone()),
&component,
OpenOptions::Resolver(open_options),
)
.await
.map_err(ResolverError::routing_error)?;
let component = proxy.resolve(component_url).await.map_err(ResolverError::fidl_error)??;
let decl_buffer: fmem::Data = component.decl.ok_or(ResolverError::RemoteInvalidData)?;
Ok(ResolvedComponent {
resolved_url: component.resolved_url.ok_or(ResolverError::RemoteInvalidData)?,
decl: read_and_validate_manifest(decl_buffer).await?,
package: component.package,
})
}
}
async fn read_and_validate_manifest(
data: fmem::Data,
) -> Result<fsys::ComponentDecl, ResolverError> {
let bytes = match data {
fmem::Data::Bytes(bytes) => bytes,
fmem::Data::Buffer(buffer) => {
let mut contents = Vec::<u8>::new();
contents.resize(buffer.size as usize, 0);
buffer.vmo.read(&mut contents, 0).map_err(ResolverError::ManifestIo)?;
contents
}
_ => return Err(ResolverError::RemoteInvalidData),
};
let component_decl: fsys::ComponentDecl = fidl::encoding::decode_persistent(&bytes)
.map_err(|err| ResolverError::manifest_invalid(err))?;
cm_fidl_validator::validate(&component_decl).map_err(|e| ResolverError::manifest_invalid(e))?;
Ok(component_decl)
}
/// Errors produced by `Resolver`.
#[derive(Debug, Error, Clone)]
pub enum ResolverError {
#[error("an unexpected error occurred: {0}")]
Internal(#[source] ClonableError),
#[error("an IO error occurred: {0}")]
Io(#[source] ClonableError),
#[error("component manifest not found: {0}")]
ManifestNotFound(#[source] ClonableError),
#[error("package not found: {0}")]
PackageNotFound(#[source] ClonableError),
#[error("component manifest invalid: {0}")]
ManifestInvalid(#[source] ClonableError),
#[error("failed to read manifest: {0}")]
ManifestIo(Status),
#[error("Model not available")]
ModelNotAvailable,
#[error("scheme not registered")]
SchemeNotRegistered,
#[error("malformed url: {0}")]
MalformedUrl(#[source] ClonableError),
#[error("url missing resource")]
UrlMissingResource,
#[error("failed to route resolver capability: {0}")]
RoutingError(#[source] Box<ModelError>),
#[error("the remote resolver returned invalid data")]
RemoteInvalidData,
#[error("an error occurred sending a FIDL request to the remote resolver: {0}")]
FidlError(#[source] ClonableError),
}
impl ResolverError {
pub fn internal(err: impl Into<Error>) -> ResolverError {
ResolverError::Internal(err.into().into())
}
pub fn io(err: impl Into<Error>) -> ResolverError {
ResolverError::Io(err.into().into())
}
pub fn manifest_not_found(err: impl Into<Error>) -> ResolverError {
ResolverError::ManifestNotFound(err.into().into())
}
pub fn package_not_found(err: impl Into<Error>) -> ResolverError {
ResolverError::PackageNotFound(err.into().into())
}
pub fn manifest_invalid(err: impl Into<Error>) -> ResolverError {
ResolverError::ManifestInvalid(err.into().into())
}
pub fn malformed_url(err: impl Into<Error>) -> ResolverError {
ResolverError::MalformedUrl(err.into().into())
}
pub fn routing_error(err: impl Into<ModelError>) -> ResolverError {
ResolverError::RoutingError(Box::new(err.into()))
}
pub fn fidl_error(err: impl Into<Error>) -> ResolverError {
ResolverError::FidlError(err.into().into())
}
}
impl From<fsys::ResolverError> for ResolverError {
fn from(err: fsys::ResolverError) -> ResolverError {
match err {
fsys::ResolverError::Internal => ResolverError::internal(RemoteError(err)),
fsys::ResolverError::Io => ResolverError::io(RemoteError(err)),
fsys::ResolverError::PackageNotFound
| fsys::ResolverError::NoSpace
| fsys::ResolverError::ResourceUnavailable
| fsys::ResolverError::NotSupported => {
ResolverError::package_not_found(RemoteError(err))
}
fsys::ResolverError::ManifestNotFound => {
ResolverError::manifest_not_found(RemoteError(err))
}
fsys::ResolverError::InvalidArgs => ResolverError::malformed_url(RemoteError(err)),
}
}
}
#[derive(Error, Clone, Debug)]
#[error("remote resolver responded with {0:?}")]
struct RemoteError(fsys::ResolverError);
#[cfg(test)]
mod tests {
use {super::*, anyhow::format_err};
struct MockOkResolver {
pub expected_url: String,
pub resolved_url: String,
}
#[async_trait]
impl Resolver for MockOkResolver {
async fn resolve(&self, component_url: &str) -> Result<ResolvedComponent, ResolverError> {
assert_eq!(self.expected_url, component_url);
Ok(ResolvedComponent {
resolved_url: self.resolved_url.clone(),
decl: 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,
})
}
}
struct MockErrorResolver {
pub expected_url: String,
pub error: Box<dyn Fn(&str) -> ResolverError + Send + Sync + 'static>,
}
#[async_trait]
impl Resolver for MockErrorResolver {
async fn resolve(&self, component_url: &str) -> Result<ResolvedComponent, ResolverError> {
assert_eq!(self.expected_url, component_url);
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(|_| {
ResolverError::manifest_not_found(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);
// Resolve a different scheme that produces an error.
let expected_res: Result<ResolvedComponent, ResolverError> =
Err(ResolverError::manifest_not_found(format_err!("not available")));
assert_eq!(
format!("{:?}", expected_res),
format!("{:?}", registry.resolve("bar://url").await)
);
// Resolve an unknown scheme
let expected_res: Result<ResolvedComponent, 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<ResolvedComponent, ResolverError> =
Err(ResolverError::malformed_url(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));
}
}