blob: 0d9bc0b8c45ac2d7095d9d4d69f8fb5a6a6e75c9 [file] [log] [blame]
// Copyright 2023 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::WeakComponentInstance;
use ::routing::error::RoutingError;
use async_trait::async_trait;
use bedrock_error::{BedrockError, Explain};
use cm_types::Availability;
use fidl_fuchsia_component_sandbox as fsandbox;
use fidl_fuchsia_io as fio;
use fuchsia_zircon as zx;
use futures::future::BoxFuture;
use futures::FutureExt;
use sandbox::{AnyCapability, Capability, CapabilityTrait, Dict, Open};
use std::{fmt, sync::Arc};
use vfs::directory::entry::{self, DirectoryEntry, DirectoryEntryAsync, EntryInfo};
/// Types that implement [`Routable`] let the holder asynchronously request
/// capabilities from them.
#[async_trait]
pub trait Routable: Send + Sync {
async fn route(&self, request: Request) -> Result<Capability, BedrockError>;
}
/// [`Request`] contains metadata around how to obtain a capability.
#[derive(Debug, Clone)]
pub struct Request {
/// The minimal availability strength of the capability demanded by the requestor.
pub availability: Availability,
/// A reference to the requesting component.
pub target: WeakComponentInstance,
}
/// A [`Router`] is a capability that lets the holder obtain other capabilities
/// asynchronously. [`Router`] is the object capability representation of [`Routable`].
///
/// During routing, a request usually traverses through the component topology,
/// passing through several routers, ending up at some router that will fulfill
/// the request instead of forwarding it upstream.
#[derive(Clone)]
pub struct Router {
routable: Arc<dyn Routable>,
}
impl fmt::Debug for Router {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// TODO(https://fxbug.dev/329680070): Require `Debug` on `Routable` trait.
f.debug_struct("Router").field("routable", &"[some routable object]").finish()
}
}
// TODO(b/314343346): Complete or remove the Router implementation of sandbox::Capability
impl CapabilityTrait for Router {}
impl From<Router> for Capability {
fn from(router: Router) -> Self {
Capability::Router(Box::new(router))
}
}
/// Syntax sugar within the framework to express custom routing logic using a function
/// that takes a request and returns such future.
impl<F> Routable for F
where
F: Fn(Request) -> BoxFuture<'static, Result<Capability, BedrockError>> + Send + Sync + 'static,
{
// We use the desugared form of `async_trait` to avoid unnecessary boxing.
fn route<'a, 'b>(&'a self, request: Request) -> BoxFuture<'b, Result<Capability, BedrockError>>
where
'a: 'b,
Self: 'b,
{
self(request)
}
}
#[async_trait]
impl Routable for Router {
async fn route(&self, request: Request) -> Result<Capability, BedrockError> {
Router::route(self, request).await
}
}
impl Router {
/// Package a [`Routable`] object into a [`Router`].
pub fn new(routable: impl Routable + 'static) -> Self {
Router { routable: Arc::new(routable) }
}
/// Creates a router that will always resolve with the provided capability,
/// unless the capability is also a router, where it will recursively request
/// from the router.
pub fn new_ok(capability: impl Into<Capability>) -> Self {
let capability: Capability = capability.into();
Router::new(capability)
}
/// Creates a router that will always fail a request with the provided error.
pub fn new_error(error: impl Into<BedrockError>) -> Self {
let error: BedrockError = error.into();
Router::new(error)
}
pub fn from_any(any: AnyCapability) -> Router {
*any.into_any().downcast::<Router>().unwrap()
}
/// Obtain a capability from this router, following the description in `request`.
pub async fn route(&self, request: Request) -> Result<Capability, BedrockError> {
self.routable.route(request).await
}
/// Returns a router that ensures the capability request has an availability
/// strength that is at least the provided `availability`.
pub fn with_availability(self, availability: Availability) -> Router {
let route_fn = move |mut request: Request| {
let router = self.clone();
async move {
// The availability of the request must be compatible with the
// availability of this step of the route.
match ::routing::availability::advance(request.availability, availability) {
Ok(updated) => {
request.availability = updated;
// Everything checks out, forward the request.
let res = router.route(request).await;
res
}
Err(e) => Err(RoutingError::from(e).into()),
}
}
.boxed()
};
Router::new(route_fn)
}
/// Returns a [Dict] equivalent to `dict`, but with all [Router]s replaced with [Open].
///
/// This is an alternative to [Dict::try_into_open] when the [Dict] contains [Router]s, since
/// [Router] is not currently a type defined by the sandbox library.
pub fn dict_routers_to_open(weak_component: &WeakComponentInstance, dict: &Dict) -> Dict {
let entries = dict.lock_entries();
let out = Dict::new();
let mut out_entries = out.lock_entries();
for (key, value) in entries.iter() {
let value = match value {
Capability::Dictionary(dict) => {
Capability::Dictionary(Self::dict_routers_to_open(weak_component, dict))
}
Capability::Router(r) => {
let router = Router::from_any(r.clone());
let request = Request {
target: weak_component.clone(),
// Use the weakest availability, so that it gets immediately upgraded to
// the availability in `router`.
availability: cm_types::Availability::Transitional,
};
// TODO: Should we convert the Open to a Directory here if the Router wraps a
// Dict?
Capability::Open(Open::new(router.into_directory_entry(
request,
fio::DirentType::Service,
|_| None,
)))
}
other => other.clone(),
};
out_entries.insert(key.clone(), value.clone()).ok();
}
drop(out_entries);
out
}
/// Converts the [Router] capability into DirectoryEntry such that open requests
/// will be fulfilled via the specified `request` on the router.
///
/// `entry_type` is the type of the entry when the DirectoryEntry is accessed through a `fuchsia.io`
/// connection.
///
/// Tasks are spawned on the component's execution scope.
///
/// When routing failed while exercising the returned DirectoryEntry, errors will be
/// sent to `errors_fn`.
pub fn into_directory_entry<F>(
self,
request: Request,
entry_type: fio::DirentType,
errors_fn: F,
) -> Arc<dyn DirectoryEntry>
where
for<'a> F: Fn(&'a BedrockError) -> Option<BoxFuture<'a, ()>> + Send + Sync + 'static,
{
struct RouterEntry<F> {
router: Router,
request: Request,
entry_type: fio::DirentType,
errors_fn: F,
}
impl<F> DirectoryEntry for RouterEntry<F>
where
for<'a> F: Fn(&'a BedrockError) -> Option<BoxFuture<'a, ()>> + Send + Sync + 'static,
{
fn entry_info(&self) -> EntryInfo {
EntryInfo::new(fio::INO_UNKNOWN, self.entry_type)
}
fn open_entry(
self: Arc<Self>,
mut request: entry::OpenRequest<'_>,
) -> Result<(), zx::Status> {
if let Ok(target) = self.request.target.upgrade() {
// Spawn this request on the component's execution scope so that it doesn't
// block the namespace.
request.set_scope(target.execution_scope.clone());
request.spawn(self);
Ok(())
} else {
Err(zx::Status::NOT_FOUND)
}
}
}
impl<F> DirectoryEntryAsync for RouterEntry<F>
where
for<'a> F: Fn(&'a BedrockError) -> Option<BoxFuture<'a, ()>> + Send + Sync + 'static,
{
async fn open_entry_async(
self: Arc<Self>,
open_request: entry::OpenRequest<'_>,
) -> Result<(), zx::Status> {
// Hold a guard to prevent this task from being dropped during component
// destruction. This task is tied to the target component.
let _guard = open_request.scope().active_guard();
// Request a capability from the `router`.
let result = self.router.route(self.request.clone()).await;
let error = match result {
Ok(capability) => {
// HACK: Dict needs special casing because [Dict::try_into_open]
// is unaware of [Router].
let capability = match capability {
Capability::Dictionary(d) => {
Router::dict_routers_to_open(&self.request.target, &d).into()
}
cap => cap,
};
match super::capability_into_open(capability.clone()) {
Ok(open) => return open.open_entry(open_request),
Err(error) => error,
}
}
Err(error) => error, // Routing failed (e.g. broken route).
};
if let Some(fut) = (self.errors_fn)(&error) {
fut.await;
}
Err(error.as_zx_status())
}
}
Arc::new(RouterEntry { router: self.clone(), request, entry_type, errors_fn })
}
}
impl From<Router> for fsandbox::Capability {
fn from(_router: Router) -> Self {
unimplemented!("TODO(b/314343346): Complete or remove the Router implementation of sandbox::Capability")
}
}
#[async_trait]
impl Routable for Capability {
async fn route(&self, request: Request) -> Result<Capability, BedrockError> {
match self.clone() {
Capability::Router(router) => Router::from_any(router).route(request).await,
capability => Ok(capability),
}
}
}
#[async_trait]
impl Routable for Dict {
async fn route(&self, request: Request) -> Result<Capability, BedrockError> {
Capability::Dictionary(self.clone()).route(request).await
}
}
#[async_trait]
impl Routable for BedrockError {
async fn route(&self, _: Request) -> Result<Capability, BedrockError> {
Err(self.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use bedrock_error::DowncastErrorForTest;
use sandbox::{Data, Message, Receiver};
#[fuchsia::test]
async fn availability_good() {
let source: Capability = Data::String("hello".to_string()).into();
let base = Router::new(source);
let proxy = base.with_availability(Availability::Optional);
let capability = proxy
.route(Request {
availability: Availability::Optional,
target: WeakComponentInstance::invalid(),
})
.await
.unwrap();
let capability = match capability {
Capability::Data(d) => d,
c => panic!("Bad enum {:#?}", c),
};
assert_eq!(capability, Data::String("hello".to_string()));
}
#[fuchsia::test]
async fn availability_bad() {
let source: Capability = Data::String("hello".to_string()).into();
let base = Router::new(source);
let proxy = base.with_availability(Availability::Optional);
let error = proxy
.route(Request {
availability: Availability::Required,
target: WeakComponentInstance::invalid(),
})
.await
.unwrap_err();
use ::routing::error::AvailabilityRoutingError;
assert_matches!(
error,
BedrockError::RoutingError(err)
if matches!(
err.downcast_for_test::<RoutingError>(),
RoutingError::AvailabilityRoutingError(
AvailabilityRoutingError::TargetHasStrongerAvailability
)
)
);
}
#[fuchsia::test]
async fn route_and_use_sender_with_dropped_receiver() {
// We want to test vending a sender with a router, dropping the associated receiver, and
// then using the sender. The objective is to observe an error, and not panic.
let (receiver, sender) = Receiver::new();
let router = Router::new_ok(sender.clone());
let capability = router
.route(Request {
availability: Availability::Required,
target: WeakComponentInstance::invalid(),
})
.await
.unwrap();
let sender = match capability {
Capability::Sender(c) => c,
c => panic!("Bad enum {:#?}", c),
};
drop(receiver);
let (ch1, _ch2) = zx::Channel::create();
assert!(sender.send(Message { channel: ch1 }).is_err());
}
}