| // Copyright 2021 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::component_instance::{ComponentInstanceForAnalyzer, TopInstanceForAnalyzer}; |
| use crate::route::{TargetDecl, VerifyRouteResult}; |
| use crate::{PkgUrlMatch, match_absolute_component_urls}; |
| use anyhow::{Context, Result, anyhow}; |
| use capability_source::{ |
| CapabilitySource, CapabilityToCapabilitySource, ComponentCapability, ComponentSource, |
| }; |
| use cm_config::RuntimeConfig; |
| use cm_rust::{ |
| Availability, CapabilityTypeName, ComponentDecl, ExposeDecl, OfferDecl, OfferDeclCommon, |
| OfferTarget, ProgramDecl, SourceName, UseDecl, UseDeclCommon, UseRunnerDecl, UseSource, |
| }; |
| use cm_types::{IterablePath, Name, Url}; |
| use config_encoder::ConfigFields; |
| use fidl::prelude::*; |
| use fidl_fuchsia_sys2 as fsys; |
| use fuchsia_url::fuchsia_pkg::AbsoluteComponentUrl; |
| use futures::FutureExt; |
| use moniker::{ChildName, ExtendedMoniker, Moniker}; |
| use router_error::{Explain, RouterError}; |
| use routing::component_instance::{ComponentInstanceInterface, ExtendedInstanceInterface}; |
| use routing::error::{ComponentInstanceError, RoutingError}; |
| use routing::policy::GlobalPolicyChecker; |
| use routing::{ |
| SandboxPath, debug_route_sandbox_path, debug_route_sandbox_path_with_request, |
| debug_route_storage_backing_directory, |
| }; |
| use serde::{Deserialize, Serialize}; |
| use std::collections::{BTreeMap, HashMap, HashSet}; |
| use std::sync::Arc; |
| use thiserror::Error; |
| use zx_status; |
| |
| /// Errors that may occur when building a `ComponentModelForAnalyzer` from |
| /// a set of component manifests. |
| #[derive(Clone, Debug, Deserialize, Error, PartialEq, Serialize)] |
| #[serde(rename_all = "snake_case")] |
| pub enum BuildAnalyzerModelError { |
| #[error("no component declaration found for url `{0}` requested by node `{1}`")] |
| ComponentDeclNotFound(String, String), |
| |
| #[error("invalid child declaration containing url `{0}` at node `{1}`")] |
| InvalidChildDecl(String, String), |
| |
| #[error("no node found with path `{0}`")] |
| ComponentNodeNotFound(String), |
| |
| #[error("environment `{0}` requested by child `{1}` not found at node `{2}`")] |
| EnvironmentNotFound(String, String, String), |
| |
| #[error("multiple resolvers found for scheme `{0}`")] |
| DuplicateResolverScheme(String), |
| |
| #[error("malformed url {0} for component instance {1}")] |
| MalformedUrl(String, String), |
| |
| #[error("dynamic component with url {0} an invalid moniker")] |
| DynamicComponentInvalidMoniker(String), |
| |
| #[error("dynamic component at {0} with url {1} is not part of a collection")] |
| DynamicComponentWithoutCollection(String, String), |
| } |
| |
| /// Errors that a `ComponentModelForAnalyzer` may detect in the component graph. |
| #[derive(Clone, Debug, Error, Deserialize, Serialize, PartialEq)] |
| #[serde(rename_all = "snake_case")] |
| pub enum AnalyzerModelError { |
| #[error("the source instance `{0}` is not executable")] |
| SourceInstanceNotExecutable(Moniker), |
| |
| #[error("at component {0} the capability `{1}` is not a valid source for the capability `{2}`")] |
| InvalidSourceCapability(ExtendedMoniker, String, String), |
| |
| #[error("no resolver found in environment of component `{0}` for scheme `{1}`")] |
| MissingResolverForScheme(Moniker, String), |
| |
| #[error(transparent)] |
| ComponentInstanceError(#[from] ComponentInstanceError), |
| |
| #[error(transparent)] |
| RoutingError(#[from] RoutingError), |
| } |
| |
| impl AnalyzerModelError { |
| pub fn as_zx_status(&self) -> zx_status::Status { |
| match self { |
| Self::SourceInstanceNotExecutable(_) => zx_status::Status::NOT_FOUND, |
| Self::InvalidSourceCapability(_, _, _) => zx_status::Status::NOT_FOUND, |
| Self::MissingResolverForScheme(_, _) => zx_status::Status::NOT_FOUND, |
| Self::ComponentInstanceError(err) => err.as_zx_status(), |
| Self::RoutingError(err) => err.as_zx_status(), |
| } |
| } |
| } |
| |
| impl From<AnalyzerModelError> for ExtendedMoniker { |
| fn from(err: AnalyzerModelError) -> ExtendedMoniker { |
| match err { |
| AnalyzerModelError::InvalidSourceCapability(moniker, _, _) => moniker, |
| AnalyzerModelError::MissingResolverForScheme(moniker, _) |
| | AnalyzerModelError::SourceInstanceNotExecutable(moniker) => moniker.into(), |
| AnalyzerModelError::ComponentInstanceError(err) => err.into(), |
| AnalyzerModelError::RoutingError(err) => err.into(), |
| } |
| } |
| } |
| |
| /// Builds a `ComponentModelForAnalyzer` from a set of component manifests. |
| pub struct ModelBuilderForAnalyzer { |
| default_root_url: Url, |
| } |
| |
| /// The type returned by `ModelBuilderForAnalyzer::build()`. May contain some |
| /// errors even if `model` is `Some`. |
| pub struct BuildModelResult { |
| pub model: Option<Arc<ComponentModelForAnalyzer>>, |
| pub errors: Vec<anyhow::Error>, |
| } |
| |
| impl BuildModelResult { |
| fn new() -> Self { |
| Self { model: None, errors: Vec::new() } |
| } |
| } |
| |
| #[derive(Default)] |
| pub struct DynamicConfig { |
| pub components: HashMap<Moniker, (AbsoluteComponentUrl, Option<Name>)>, |
| pub dictionaries: DynamicDictionaryConfig, |
| } |
| |
| pub type DynamicDictionaryConfig = HashMap<Moniker, HashMap<Name, Vec<(CapabilityTypeName, Name)>>>; |
| |
| impl ModelBuilderForAnalyzer { |
| pub fn new(default_root_url: Url) -> Self { |
| Self { default_root_url } |
| } |
| |
| fn load_dynamic_components( |
| input: HashMap<Moniker, (AbsoluteComponentUrl, Option<Name>)>, |
| ) -> (HashMap<Moniker, Vec<Child>>, Vec<anyhow::Error>) { |
| let mut errors: Vec<anyhow::Error> = vec![]; |
| let mut dynamic_components: HashMap<Moniker, Vec<Child>> = HashMap::new(); |
| for (moniker, (url, environment)) in input.into_iter() { |
| let Some((parent_moniker, child_moniker)) = moniker.split_leaf() else { |
| errors.push( |
| BuildAnalyzerModelError::DynamicComponentInvalidMoniker(url.to_string()).into(), |
| ); |
| continue; |
| }; |
| if child_moniker.collection().is_none() { |
| errors.push( |
| BuildAnalyzerModelError::DynamicComponentWithoutCollection( |
| moniker.to_string(), |
| url.to_string(), |
| ) |
| .into(), |
| ); |
| continue; |
| } |
| |
| let children = dynamic_components.entry(parent_moniker).or_insert_with(|| vec![]); |
| match Url::new(&url.to_string()) { |
| Ok(url) => { |
| children.push(Child { child_moniker: child_moniker.into(), url, environment }); |
| } |
| Err(_) => { |
| errors.push( |
| BuildAnalyzerModelError::MalformedUrl(url.to_string(), moniker.to_string()) |
| .into(), |
| ); |
| } |
| } |
| } |
| (dynamic_components, errors) |
| } |
| |
| pub fn build( |
| self, |
| decls_by_url: HashMap<Url, (ComponentDecl, Option<ConfigFields>)>, |
| runtime_config: Arc<RuntimeConfig>, |
| component_id_index: Arc<component_id_index::Index>, |
| ) -> BuildModelResult { |
| self.build_with_dynamic_config( |
| DynamicConfig::default(), |
| decls_by_url, |
| runtime_config, |
| component_id_index, |
| ) |
| } |
| |
| pub fn build_with_dynamic_config( |
| self, |
| dynamic_config: DynamicConfig, |
| decls_by_url: HashMap<Url, (ComponentDecl, Option<ConfigFields>)>, |
| runtime_config: Arc<RuntimeConfig>, |
| component_id_index: Arc<component_id_index::Index>, |
| ) -> BuildModelResult { |
| let mut result = BuildModelResult::new(); |
| |
| let (dynamic_components, mut dynamic_component_errors) = |
| Self::load_dynamic_components(dynamic_config.components); |
| result.errors.append(&mut dynamic_component_errors); |
| |
| let dynamic_dictionaries = Arc::new(dynamic_config.dictionaries); |
| |
| // Initialize the model with an empty `instances` map. |
| let mut model = ComponentModelForAnalyzer { |
| top_instance: TopInstanceForAnalyzer::new( |
| runtime_config.namespace_capabilities.clone(), |
| runtime_config.builtin_capabilities.clone(), |
| ), |
| instances: HashMap::new(), |
| policy_checker: GlobalPolicyChecker::new(runtime_config.security_policy.clone()), |
| component_id_index, |
| }; |
| |
| let root_url = runtime_config.root_component_url.as_ref().unwrap_or(&self.default_root_url); |
| |
| // If `root_url` matches a `ComponentDecl` in `decls_by_url`, construct the root |
| // instance and then recursively add child instances to the model. |
| match Self::get_decl_by_url(&decls_by_url, root_url) { |
| Err(err) => { |
| result.errors.push(err.context("Failed to parse root URL as fuchsia package URL")); |
| } |
| Ok(None) => { |
| result.errors.push(anyhow!("Failed to locate root component with URL: {root_url}")); |
| } |
| Ok(Some((root_decl, root_config))) => { |
| let root_instance = ComponentInstanceForAnalyzer::new_root( |
| root_decl.clone(), |
| root_config.clone(), |
| root_url.clone(), |
| Arc::clone(&model.top_instance), |
| Arc::clone(&runtime_config), |
| model.policy_checker.clone(), |
| Arc::clone(&model.component_id_index), |
| Arc::clone(&dynamic_dictionaries), |
| ); |
| |
| Self::add_descendants( |
| &root_instance, |
| &decls_by_url, |
| &dynamic_components, |
| &dynamic_dictionaries, |
| &mut model, |
| &mut result, |
| ); |
| |
| model.instances.insert(root_instance.moniker().clone(), root_instance); |
| |
| result.model = Some(Arc::new(model)); |
| } |
| } |
| |
| result |
| } |
| |
| // Adds all descendants of `instance` to `model`, also inserting each new instance |
| // in the `children` map of its parent, including children denoted in |
| // `dynamic_components`. |
| fn add_descendants( |
| instance: &Arc<ComponentInstanceForAnalyzer>, |
| decls_by_url: &HashMap<Url, (ComponentDecl, Option<ConfigFields>)>, |
| dynamic_components: &HashMap<Moniker, Vec<Child>>, |
| dynamic_dictionaries: &Arc<DynamicDictionaryConfig>, |
| model: &mut ComponentModelForAnalyzer, |
| result: &mut BuildModelResult, |
| ) { |
| let mut children = vec![]; |
| for child_decl in instance.decl.children.iter() { |
| let child_moniker = ChildName::new(child_decl.name.clone(), None); |
| match Self::get_absolute_child_url(&child_decl.url, instance) { |
| Ok(url) => { |
| children.push(Child { |
| child_moniker, |
| url, |
| environment: child_decl.environment.clone(), |
| }); |
| } |
| Err(err) => { |
| result.errors.push(anyhow!(err)); |
| } |
| } |
| } |
| if let Some(dynamic_children) = dynamic_components.get(instance.moniker()) { |
| children.append( |
| &mut dynamic_children.iter().map(|dynamic_child| dynamic_child.clone()).collect(), |
| ); |
| } |
| |
| for child in children.iter() { |
| if child.child_moniker.name().is_empty() { |
| result.errors.push(anyhow!(BuildAnalyzerModelError::InvalidChildDecl( |
| child.url.to_string(), |
| instance.moniker().to_string(), |
| ))); |
| continue; |
| } |
| |
| match Self::get_decl_by_url(decls_by_url, &child.url) |
| .context("Failed to parse absolute child URL") |
| { |
| Err(err) => { |
| result.errors.push(err); |
| } |
| Ok(Some((child_component_decl, child_config))) => { |
| match ComponentInstanceForAnalyzer::new_for_child( |
| child, |
| child_component_decl.clone(), |
| child_config.clone(), |
| Arc::clone(instance), |
| model.policy_checker.clone(), |
| Arc::clone(&model.component_id_index), |
| Arc::clone(&dynamic_dictionaries), |
| ) { |
| Ok(child_instance) => { |
| Self::add_descendants( |
| &child_instance, |
| decls_by_url, |
| dynamic_components, |
| dynamic_dictionaries, |
| model, |
| result, |
| ); |
| |
| instance.add_child( |
| child.child_moniker.clone(), |
| Arc::clone(&child_instance), |
| ); |
| |
| model |
| .instances |
| .insert(child_instance.moniker().clone(), child_instance); |
| } |
| Err(err) => { |
| result.errors.push(anyhow!(err)); |
| } |
| } |
| } |
| Ok(None) => { |
| result.errors.push(anyhow!(BuildAnalyzerModelError::ComponentDeclNotFound( |
| child.url.to_string(), |
| instance.moniker().to_string(), |
| ))); |
| } |
| } |
| } |
| } |
| |
| // Given a component instance and the url `child_url` of a child of that instance, |
| // returns an absolute url for the child. |
| fn get_absolute_child_url( |
| child_url: &Url, |
| instance: &Arc<ComponentInstanceForAnalyzer>, |
| ) -> Result<Url, BuildAnalyzerModelError> { |
| let child_url = child_url.as_str(); |
| let err = BuildAnalyzerModelError::MalformedUrl( |
| instance.url().to_string(), |
| instance.moniker().to_string(), |
| ); |
| |
| let url = match url::Url::parse(child_url) { |
| Ok(u) => u, |
| Err(url::ParseError::RelativeUrlWithoutBase) => { |
| let absolute_prefix = match instance.url().is_relative() { |
| true => find_first_absolute_ancestor_url(instance).map_err(|_| err.clone())?, |
| false => instance.url().clone(), |
| }; |
| let absolute_prefix = |
| url::Url::parse(absolute_prefix.as_str()).map_err(|_| err.clone())?; |
| absolute_prefix |
| .join(child_url) |
| .expect("failed to join child URL to absolute prefix") |
| } |
| _ => return Err(err), |
| }; |
| Url::new(url.as_str()).map_err(|_| err.clone()) |
| } |
| |
| fn get_decl_by_url<'a>( |
| decls_by_url: &'a HashMap<Url, (ComponentDecl, Option<ConfigFields>)>, |
| url: &Url, |
| ) -> Result<Option<&'a (ComponentDecl, Option<ConfigFields>)>> { |
| // Non-`fuchsia-pkg` URLs are not matched with nuance: they must precisely match an entry |
| // in `decls_by_url`. |
| if url.scheme().expect("all urls are absolute") != "fuchsia-pkg" { |
| return Ok(decls_by_url.get(url)); |
| } |
| |
| let fuchsia_component_url = AbsoluteComponentUrl::parse(url.as_str()) |
| .context("Failed to parse component fuchsia-pkg URL as absolute package URL")?; |
| |
| // Gather both strong and weak URL matches against `fuchsia_component_url`. |
| let decl_url_matches = decls_by_url |
| .keys() |
| .filter_map(|decl_url| { |
| if decl_url.scheme().expect("all urls are absolute") != "fuchsia-pkg" { |
| None |
| } else if let Ok(decl_fuchsia_pkg_url) = |
| AbsoluteComponentUrl::parse(decl_url.as_str()) |
| { |
| match match_absolute_component_urls( |
| &decl_fuchsia_pkg_url, |
| &fuchsia_component_url, |
| ) { |
| PkgUrlMatch::NoMatch => None, |
| pkg_url_match => Some((decl_url, pkg_url_match)), |
| } |
| } else { |
| None |
| } |
| }) |
| .collect::<Vec<(&Url, PkgUrlMatch)>>(); |
| |
| // Return best match. Emit warning or error when multiple matches are found. |
| if decl_url_matches.len() == 0 { |
| return Ok(None); |
| } else if decl_url_matches.len() == 1 { |
| if decl_url_matches[0].1 == PkgUrlMatch::WeakMatch { |
| log::warn!("Weak component URL match: {} matches {}", url, decl_url_matches[0].0); |
| } |
| return Ok(decls_by_url.get(decl_url_matches[0].0)); |
| } else { |
| let strong_decl_url_matches = decl_url_matches |
| .iter() |
| .filter_map(|(url, url_match)| match url_match { |
| PkgUrlMatch::StrongMatch => Some(*url), |
| _ => None, |
| }) |
| .collect::<Vec<&Url>>(); |
| |
| if strong_decl_url_matches.len() == 0 { |
| log::warn!( |
| "Multiple weak component URL matches for {}; matching to first: {}", |
| url, |
| decl_url_matches[0].0 |
| ); |
| return Ok(decls_by_url.get(decl_url_matches[0].0)); |
| } else { |
| if strong_decl_url_matches.len() > 1 { |
| log::error!( |
| "Multiple strong package URL matches for {}; matching to first: {}", |
| url, |
| strong_decl_url_matches[0] |
| ); |
| } |
| return Ok(decls_by_url.get(strong_decl_url_matches[0])); |
| } |
| } |
| } |
| } |
| |
| fn find_first_absolute_ancestor_url( |
| component: &Arc<ComponentInstanceForAnalyzer>, |
| ) -> Result<Url, ComponentInstanceError> { |
| let mut parent = component.try_get_parent()?; |
| loop { |
| match parent { |
| ExtendedInstanceInterface::Component(parent_component) => { |
| if !parent_component.url().is_relative() { |
| return Ok(parent_component.url().clone()); |
| } |
| parent = parent_component.try_get_parent()?; |
| } |
| ExtendedInstanceInterface::AboveRoot(_) => { |
| return Err(ComponentInstanceError::NoAbsoluteUrl { |
| url: component.url().to_string(), |
| moniker: component.moniker().clone(), |
| }); |
| } |
| } |
| } |
| } |
| |
| /// `ComponentModelForAnalyzer` owns a representation of the v2 component graph and |
| /// supports lookup of component instances by `Moniker`. |
| #[derive(Debug, Default, Clone)] |
| pub struct ComponentModelForAnalyzer { |
| top_instance: Arc<TopInstanceForAnalyzer>, |
| instances: HashMap<Moniker, Arc<ComponentInstanceForAnalyzer>>, |
| policy_checker: GlobalPolicyChecker, |
| component_id_index: Arc<component_id_index::Index>, |
| } |
| |
| impl ComponentModelForAnalyzer { |
| /// Returns the number of component instances in the model, not counting the top instance. |
| pub fn len(&self) -> usize { |
| self.instances.len() |
| } |
| |
| pub fn get_root_instance( |
| self: &Arc<Self>, |
| ) -> Result<Arc<ComponentInstanceForAnalyzer>, ComponentInstanceError> { |
| self.get_instance(&Moniker::root()) |
| } |
| |
| /// Returns the component instance corresponding to `id` if it is present in the model, or an |
| /// `InstanceNotFound` error if not. |
| pub fn get_instance( |
| self: &Arc<Self>, |
| moniker: &Moniker, |
| ) -> Result<Arc<ComponentInstanceForAnalyzer>, ComponentInstanceError> { |
| match self.instances.get(moniker) { |
| Some(instance) => Ok(Arc::clone(instance)), |
| None => Err(ComponentInstanceError::instance_not_found(moniker.clone())), |
| } |
| } |
| |
| fn does_child_reference_offer(self: &Arc<Self>, offer: &OfferDecl, child: Moniker) -> bool { |
| let instance = if let Ok(i) = self.get_instance(&child.into()) { |
| i |
| } else { |
| // We couldn't find the instance that references this offer. |
| return false; |
| }; |
| |
| // Look for a use from parent |
| for use_ in &instance.decl.uses { |
| if use_.source_name() == offer.target_name() { |
| match use_.source() { |
| cm_rust::UseSource::Parent => return true, |
| _ => {} |
| } |
| } |
| } |
| |
| // Look for a next offer from parent |
| for next_offer in &instance.decl.offers { |
| if next_offer.source_name() == offer.target_name() { |
| match next_offer.source() { |
| cm_rust::offer::OfferSource::Parent => return true, |
| _ => {} |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| /// For this offer decl, if the offer target does not reference the capability in its manifest, |
| /// attempt to route it and report any errors. |
| /// |
| /// In other words, this will only verify offer decls that terminate the route chain. |
| async fn try_check_offer_capability( |
| self: &Arc<Self>, |
| offer_decl: &OfferDecl, |
| target: &Arc<ComponentInstanceForAnalyzer>, |
| ) -> Vec<VerifyRouteResult> { |
| let target_moniker = target.moniker(); |
| |
| let offer_target = offer_decl.target(); |
| let should_check_offer = match offer_target { |
| OfferTarget::Child(c) => { |
| let child = ChildName::parse(&c.name).unwrap(); |
| let offer_target_moniker = target_moniker.child(child); |
| |
| // This offer should be checked if there is no reference to it in the child. |
| !self.does_child_reference_offer(offer_decl, offer_target_moniker) |
| } |
| OfferTarget::Collection(_) => { |
| // Offering to a collection should always cause an offer check. |
| true |
| } |
| OfferTarget::Capability(_) => { |
| // Offering to a dictionary (aggregation) should always cause an offer check. |
| true |
| } |
| }; |
| |
| if should_check_offer { |
| self.check_offer_capability(offer_decl, target).await |
| } else { |
| // This offer decl doesn't need to be checked. |
| vec![] |
| } |
| } |
| |
| /// Performs a debug route on the router at `sandbox_path` in the component's sandbox. Policy |
| /// checks will be disabled when `skip_policy_check` is true, which is necessary when routing |
| /// things from non-terminal points in the route (for example, an offer). A `VerifyRouteResult` |
| /// is constructed from the route results and `target_decl`. |
| /// |
| /// If the route results indicate that the routed capability is a storage capability, then a |
| /// second route is performed to find the source of the storage capability's backing directory, |
| /// and a second `VerifyRouteResult` is created and returned along with the first. |
| /// |
| /// It is safe to assume that only one `VerifyRouteResult` will be returned if `target_decl` is |
| /// not for a storage capability. |
| async fn route_sandbox_path( |
| self: &Arc<Self>, |
| target: &Arc<ComponentInstanceForAnalyzer>, |
| sandbox_path: impl Into<SandboxPath>, |
| target_decl: TargetDecl, |
| skip_policy_check: bool, |
| ) -> Vec<VerifyRouteResult> { |
| let mut results = vec![]; |
| let process_route_result = |res: Result<CapabilitySource, RoutingError>| { |
| let self_clone = self.clone(); |
| let target_decl = target_decl.clone(); |
| async move { |
| let source_check = match res.clone() { |
| Ok(source) => self_clone.check_use_source(&source, target).await, |
| Err(e) => Err(e.into()), |
| }; |
| VerifyRouteResult { |
| using_node: target.moniker().clone(), |
| capability: target_decl.source_name(), |
| target_decl, |
| error: source_check.err(), |
| source: res.clone().ok(), |
| } |
| } |
| }; |
| let mut request = target_decl.to_route_request(); |
| request.skip_policy_checks = Some(skip_policy_check); |
| let res = debug_route_sandbox_path_with_request(target, sandbox_path, request) |
| .now_or_never() |
| .expect("future was not ready immediately"); |
| results.push(process_route_result(res.clone()).await); |
| |
| if let Ok(source) = res { |
| if let CapabilitySource::Component(ComponentSource { |
| capability: ComponentCapability::Storage(storage_decl), |
| moniker, |
| .. |
| }) = &source |
| { |
| if let Ok(storage_component) = target.find_absolute(moniker).await { |
| let res = debug_route_storage_backing_directory( |
| &storage_component, |
| storage_decl.clone(), |
| ) |
| .now_or_never() |
| .expect("future was not ready immediately"); |
| results.push(process_route_result(res).await); |
| } |
| } |
| } |
| results |
| } |
| |
| pub async fn check_offer_capability( |
| self: &Arc<Self>, |
| offer_decl: &OfferDecl, |
| target: &Arc<ComponentInstanceForAnalyzer>, |
| ) -> Vec<VerifyRouteResult> { |
| let results = self |
| .route_sandbox_path(target, offer_decl, TargetDecl::Offer(offer_decl.clone()), true) |
| .await; |
| // Ignore any valid routes to void. |
| results |
| .into_iter() |
| .filter(|r| !matches!(r.source, Some(CapabilitySource::Void(_)))) |
| .collect() |
| } |
| |
| /// Checks the routing for all capabilities of the specified types that are `used` by `target`. |
| pub async fn check_routes_for_instance( |
| self: &Arc<Self>, |
| target: &Arc<ComponentInstanceForAnalyzer>, |
| capability_types: &HashSet<CapabilityTypeName>, |
| ) -> HashMap<CapabilityTypeName, Vec<VerifyRouteResult>> { |
| let mut results = HashMap::new(); |
| for capability_type in capability_types.iter() { |
| results.insert(capability_type.clone(), vec![]); |
| } |
| |
| for use_decl in target.decl.uses.iter().filter(|&u| capability_types.contains(&u.into())) { |
| let type_results = results |
| .get_mut(&CapabilityTypeName::from(use_decl)) |
| .expect("expected results for capability type"); |
| for result in self.check_use_capability(use_decl, &target).await { |
| type_results.push(result); |
| } |
| } |
| |
| for expose_decl in |
| target.decl.exposes.iter().filter(|&e| capability_types.contains(&e.into())) |
| { |
| let type_results = results |
| .get_mut(&CapabilityTypeName::from(expose_decl)) |
| .expect("expected results for capability type"); |
| if let Some(result) = self.check_use_exposed_capability(expose_decl, &target).await { |
| type_results.push(result); |
| } |
| } |
| |
| for offer_decl in |
| target.decl.offers.iter().filter(|&o| capability_types.contains(&o.into())) |
| { |
| let type_results = results |
| .get_mut(&CapabilityTypeName::from(offer_decl)) |
| .expect("expected results for capability type"); |
| for result in self.try_check_offer_capability(offer_decl, &target).await { |
| type_results.push(result); |
| } |
| } |
| |
| if capability_types.contains(&CapabilityTypeName::Runner) { |
| if let Some(ref program) = target.decl.program { |
| let type_results = results |
| .get_mut(&CapabilityTypeName::Runner) |
| .expect("expected results for capability type"); |
| if let Some(result) = self.check_program_runner(program, &target) { |
| type_results.push(result); |
| } |
| } |
| } |
| |
| if capability_types.contains(&CapabilityTypeName::Resolver) { |
| let type_results = results |
| .get_mut(&CapabilityTypeName::Resolver) |
| .expect("expected results for capability type"); |
| type_results.push(self.check_resolver(&target).await); |
| } |
| |
| results |
| } |
| |
| pub async fn check_used_path( |
| path: &impl IterablePath, |
| target: &Arc<ComponentInstanceForAnalyzer>, |
| ) -> Result<CapabilitySource, RouterError> { |
| let sandbox_path = SandboxPath::used_path(path); |
| let res = debug_route_sandbox_path(target, sandbox_path) |
| .now_or_never() |
| .expect("future was not ready immediately"); |
| res.map_err(Into::into) |
| } |
| |
| /// Given a `UseDecl` for a capability at an instance `target`, first routes the capability |
| /// to its source and then validates the source. |
| /// |
| /// This returns a vector of route results because some capabilities (storage) cause |
| /// multiple route verifications (route storage + backing directory) and both results |
| /// are relevant. |
| pub async fn check_use_capability( |
| self: &Arc<Self>, |
| use_decl: &UseDecl, |
| target: &Arc<ComponentInstanceForAnalyzer>, |
| ) -> Vec<VerifyRouteResult> { |
| let results = self |
| .route_sandbox_path(target, use_decl, TargetDecl::Use(use_decl.clone()), false) |
| .await; |
| // Ignore any valid routes to void. |
| results |
| .into_iter() |
| .filter(|r| !matches!(r.source, Some(CapabilitySource::Void(_)))) |
| .collect() |
| } |
| |
| /// Given a `ExposeDecl` for a capability at an instance `target`, checks whether the capability |
| /// can be used from an expose declaration. If so, routes the capability to its source and then |
| /// validates the source. |
| pub async fn check_use_exposed_capability( |
| self: &Arc<Self>, |
| expose_decl: &ExposeDecl, |
| target: &Arc<ComponentInstanceForAnalyzer>, |
| ) -> Option<VerifyRouteResult> { |
| let mut res = self |
| .route_sandbox_path(target, expose_decl, TargetDecl::Expose(expose_decl.clone()), false) |
| .await; |
| res.pop() |
| } |
| |
| /// Given a `ProgramDecl` for a component instance, checks whether the specified runner has |
| /// a valid capability route. |
| pub fn check_program_runner( |
| self: &Arc<Self>, |
| program_decl: &ProgramDecl, |
| target: &Arc<ComponentInstanceForAnalyzer>, |
| ) -> Option<VerifyRouteResult> { |
| match program_decl.runner { |
| Some(ref runner) => { |
| let use_runner = UseRunnerDecl { |
| source: UseSource::Environment, |
| source_name: runner.clone(), |
| source_dictionary: Default::default(), |
| }; |
| self.route_sandbox_path( |
| target, |
| &UseDecl::Runner(use_runner.clone()), |
| TargetDecl::Use(use_runner.into()), |
| false, |
| ) |
| .now_or_never() |
| .expect("future was not ready immediately") |
| .pop() |
| } |
| None => None, |
| } |
| } |
| |
| /// Given a component instance, extracts the URL scheme for that instance and looks for a |
| /// resolver for that scheme in the instance's environment, recording an error if none |
| /// is found. If a resolver is found, checks that it has a valid capability route. |
| pub async fn check_resolver( |
| self: &Arc<Self>, |
| target: &Arc<ComponentInstanceForAnalyzer>, |
| ) -> VerifyRouteResult { |
| let scheme = target.url().scheme().expect("all urls are absolute"); |
| let sandbox_path = SandboxPath::resolver(&scheme); |
| let mut res = self |
| .route_sandbox_path( |
| target, |
| sandbox_path, |
| TargetDecl::ResolverFromEnvironment(scheme.clone()), |
| false, |
| ) |
| .await |
| .pop() |
| .expect("no route results when checking resolver"); |
| if let Some(AnalyzerModelError::RoutingError(RoutingError::BedrockNotPresentInDictionary { |
| name, |
| moniker, |
| })) = &res.error |
| && name.starts_with("component_input/environment/resolvers/") |
| && moniker == &target.moniker().clone().into() |
| { |
| res.error = Some(AnalyzerModelError::MissingResolverForScheme( |
| target.moniker().clone(), |
| scheme, |
| )); |
| } |
| res |
| } |
| |
| // Checks properties of a capability source that are necessary to use the capability |
| // and that are possible to verify statically. |
| async fn check_use_source( |
| &self, |
| source: &CapabilitySource, |
| target: &Arc<ComponentInstanceForAnalyzer>, |
| ) -> Result<(), AnalyzerModelError> { |
| match &source { |
| CapabilitySource::Component(ComponentSource { moniker, .. }) => { |
| let source_component = target.find_absolute(&moniker).await?; |
| self.check_executable(&source_component) |
| } |
| CapabilitySource::Namespace(_) => Ok(()), |
| CapabilitySource::Capability(CapabilityToCapabilitySource { |
| source_capability, |
| moniker: _, |
| }) => self.check_capability_source(&source_capability, source.source_moniker()), |
| CapabilitySource::Builtin(_) => Ok(()), |
| CapabilitySource::Framework(_) => Ok(()), |
| CapabilitySource::Void(_) => Ok(()), |
| _ => unimplemented![], |
| } |
| } |
| |
| // A helper function validating a source of type `Capability`. |
| // The only capability which may have a source of another capability is the `StorageAdmin` |
| // protocol. We confirm that the source is a storage capability. |
| fn check_capability_source( |
| &self, |
| source_capability: &ComponentCapability, |
| source_moniker: ExtendedMoniker, |
| ) -> Result<(), AnalyzerModelError> { |
| match source_capability { |
| ComponentCapability::Storage(_) | ComponentCapability::Directory(_) => Ok(()), |
| _ => Err(AnalyzerModelError::InvalidSourceCapability( |
| source_moniker, |
| format!("{:?}", source_capability.source_name()), |
| fsys::StorageAdminMarker::PROTOCOL_NAME.to_string(), |
| )), |
| } |
| } |
| |
| // A helper function checking whether a component instance is executable. |
| fn check_executable( |
| &self, |
| component: &Arc<ComponentInstanceForAnalyzer>, |
| ) -> Result<(), AnalyzerModelError> { |
| match component.decl.program { |
| Some(_) => Ok(()), |
| None => { |
| Err(AnalyzerModelError::SourceInstanceNotExecutable(component.moniker().clone())) |
| } |
| } |
| } |
| |
| pub fn collect_config_by_url(&self) -> anyhow::Result<BTreeMap<String, ConfigFields>> { |
| let mut configs = BTreeMap::new(); |
| for instance in self.instances.values() { |
| let mut fields = match instance.config_fields() { |
| Some(f) => f.clone(), |
| None => { |
| let Some(ref config_decl) = instance.decl.config else { |
| continue; |
| }; |
| ConfigFields { fields: Vec::new(), checksum: config_decl.checksum.clone() } |
| } |
| }; |
| |
| for use_ in instance.decl.uses.iter() { |
| let cm_rust::UseDecl::Config(config) = use_ else { |
| continue; |
| }; |
| let value = match debug_route_sandbox_path(&instance, use_) |
| .now_or_never() |
| .expect("future was not ready immediately") |
| { |
| Ok(source) => source_to_value(&config.default, source)?, |
| Err(e) |
| if config.availability == Availability::Transitional |
| && e.as_zx_status() == zx_status::Status::NOT_FOUND => |
| { |
| config.default.clone() |
| } |
| Err(e) => return Err(e.into()), |
| }; |
| let Some(value) = value else { |
| continue; |
| }; |
| |
| let new_field = config_encoder::ConfigField { |
| key: config.target_name.clone().into(), |
| value, |
| mutability: Default::default(), |
| }; |
| |
| let mut needs_key = true; |
| for field in &mut fields.fields { |
| if field.key != new_field.key { |
| continue; |
| } |
| field.value = new_field.value.clone(); |
| needs_key = false; |
| } |
| if needs_key { |
| fields.fields.push(new_field); |
| } |
| } |
| |
| configs.insert(instance.url().to_string(), fields.clone()); |
| } |
| Ok(configs) |
| } |
| } |
| |
| fn source_to_value( |
| default: &Option<cm_rust::ConfigValue>, |
| source: CapabilitySource, |
| ) -> Result<Option<cm_rust::ConfigValue>, RoutingError> { |
| let moniker = source.source_moniker(); |
| let cap = match source { |
| CapabilitySource::Void(_) => { |
| return Ok(default.clone()); |
| } |
| CapabilitySource::Capability(CapabilityToCapabilitySource { |
| source_capability, .. |
| }) => source_capability, |
| CapabilitySource::Component(ComponentSource { capability, .. }) => capability, |
| o => { |
| let type_name = |
| o.type_name().map(|t| t.to_string()).unwrap_or_else(|| "<unknown>".to_string()); |
| return Err(RoutingError::unsupported_route_source(moniker, type_name)); |
| } |
| }; |
| |
| let cap = match cap { |
| ComponentCapability::Config(c) => c, |
| c => { |
| return Err(RoutingError::unsupported_capability_type(moniker, c.type_name())); |
| } |
| }; |
| Ok(Some(cap.value)) |
| } |
| |
| #[derive(Clone, Debug)] |
| pub struct Child { |
| pub child_moniker: ChildName, |
| pub url: Url, |
| pub environment: Option<Name>, |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use anyhow::Result; |
| use assert_matches::assert_matches; |
| use cm_config::RuntimeConfig; |
| use cm_rust::{ComponentDecl, RegistrationSource, ResolverRegistration, RunnerRegistration}; |
| use cm_rust_testing::{ |
| CapabilityBuilder, ChildBuilder, ComponentDeclBuilder, EnvironmentBuilder, UseBuilder, |
| }; |
| use cm_types::{Name, RelativePath, Url}; |
| use config_encoder::ConfigFields; |
| use fidl_fuchsia_component_decl as fdecl; |
| use fidl_fuchsia_component_internal as component_internal; |
| use maplit::hashmap; |
| use moniker::{ChildName, ExtendedMoniker, Moniker}; |
| use routing::bedrock::request_metadata::{resolver_metadata, runner_metadata}; |
| use routing::component_instance::{ComponentInstanceInterface, ExtendedInstanceInterface}; |
| use routing::error::ComponentInstanceError; |
| use runtime_capabilities::Capability; |
| use std::collections::HashMap; |
| use std::sync::Arc; |
| |
| const TEST_URL_PREFIX: &str = "test:///"; |
| const BOOT_SCHEME: &str = "fuchsia-boot"; |
| |
| fn make_test_url(component_name: &str) -> Url { |
| Url::new(&format!("{}{}", TEST_URL_PREFIX, component_name)).unwrap() |
| } |
| |
| fn make_decl_map( |
| components: Vec<(&'static str, ComponentDecl)>, |
| ) -> HashMap<Url, (ComponentDecl, Option<ConfigFields>)> { |
| HashMap::from_iter( |
| components.into_iter().map(|(name, decl)| (make_test_url(name), (decl, None))), |
| ) |
| } |
| |
| // Builds a model with structure `root -- child`, retrieves each of the 2 resulting component |
| // instances, and tests their public methods. |
| #[fuchsia::test] |
| fn build_model() -> Result<()> { |
| let components = vec![ |
| ("root", ComponentDeclBuilder::new().child_default("child").build()), |
| ("child", ComponentDeclBuilder::new().build()), |
| ]; |
| |
| let config = Arc::new(RuntimeConfig::default()); |
| let url = make_test_url("root"); |
| let build_model_result = ModelBuilderForAnalyzer::new(url).build( |
| make_decl_map(components), |
| config, |
| Arc::new(component_id_index::Index::default()), |
| ); |
| assert_eq!(build_model_result.errors.len(), 0); |
| assert!(build_model_result.model.is_some()); |
| let model = build_model_result.model.unwrap(); |
| assert_eq!(model.len(), 2); |
| |
| let root_instance = model.get_instance(&Moniker::root()).expect("root instance"); |
| let child_instance = |
| model.get_instance(&Moniker::parse_str("child").unwrap()).expect("child instance"); |
| |
| let other_moniker = Moniker::parse_str("other").unwrap(); |
| let get_other_result = model.get_instance(&other_moniker); |
| assert_eq!( |
| get_other_result.err().unwrap().to_string(), |
| ComponentInstanceError::instance_not_found( |
| Moniker::parse_str(&other_moniker.to_string()).unwrap() |
| ) |
| .to_string() |
| ); |
| |
| assert_eq!(root_instance.moniker(), &Moniker::root()); |
| assert_eq!(child_instance.moniker(), &Moniker::parse_str("child").unwrap()); |
| |
| match root_instance.try_get_parent()? { |
| ExtendedInstanceInterface::AboveRoot(_) => {} |
| _ => panic!("root instance's parent should be `AboveRoot`"), |
| } |
| match child_instance.try_get_parent()? { |
| ExtendedInstanceInterface::Component(component) => { |
| assert_eq!(component.moniker(), root_instance.moniker()); |
| } |
| _ => panic!("child instance's parent should be root component"), |
| } |
| |
| let get_child = root_instance |
| .resolve() |
| .map(|locked| locked.get_child(&ChildName::try_new("child", None).unwrap()))?; |
| assert!(get_child.is_some()); |
| assert_eq!(get_child.as_ref().unwrap().moniker(), child_instance.moniker()); |
| |
| assert!(root_instance.resolve().is_ok()); |
| assert!(child_instance.resolve().is_ok()); |
| |
| Ok(()) |
| } |
| |
| // Builds a model with structure `root -- child` where the child's URL is expressed in |
| // the root manifest as a relative URL. |
| #[fuchsia::test] |
| fn build_model_with_relative_url() { |
| let root_decl = ComponentDeclBuilder::new() |
| .child(ChildBuilder::new().name("child").url("#child").build()) |
| .build(); |
| let child_decl = ComponentDeclBuilder::new().build(); |
| let root_url = make_test_url("root"); |
| let absolute_child_url = Url::new(&format!("{}#child", root_url)).unwrap(); |
| |
| let mut decls_by_url = HashMap::new(); |
| decls_by_url.insert(root_url.clone(), (root_decl, None)); |
| decls_by_url.insert(absolute_child_url.clone(), (child_decl, None)); |
| |
| let config = Arc::new(RuntimeConfig::default()); |
| let build_model_result = ModelBuilderForAnalyzer::new(root_url).build( |
| decls_by_url, |
| config, |
| Arc::new(component_id_index::Index::default()), |
| ); |
| assert_eq!(build_model_result.errors.len(), 0); |
| assert!(build_model_result.model.is_some()); |
| let model = build_model_result.model.unwrap(); |
| assert_eq!(model.len(), 2); |
| |
| let child_instance = |
| model.get_instance(&Moniker::parse_str("child").unwrap()).expect("child instance"); |
| |
| assert_eq!(child_instance.url(), &absolute_child_url); |
| } |
| |
| // Spot-checks that `route_capability` returns immediately when routing a capability from a |
| // `ComponentInstanceForAnalyzer`. In addition, updates to that method should |
| // be reviewed to make sure that this property holds; otherwise, `ComponentModelForAnalyzer`'s |
| // sync methods may panic. |
| #[fuchsia::test] |
| fn route_capability_is_sync() { |
| let components = vec![("root", ComponentDeclBuilder::new().build())]; |
| |
| let config = Arc::new(RuntimeConfig::default()); |
| let url = make_test_url("root"); |
| let build_model_result = ModelBuilderForAnalyzer::new(url).build( |
| make_decl_map(components), |
| config, |
| Arc::new(component_id_index::Index::default()), |
| ); |
| assert_eq!(build_model_result.errors.len(), 0); |
| assert!(build_model_result.model.is_some()); |
| let model = build_model_result.model.unwrap(); |
| assert_eq!(model.len(), 1); |
| |
| let root_instance = model.get_instance(&Moniker::root()).expect("root instance"); |
| |
| // Panics if the future returned by `route_sandbox_path` was not ready immediately. |
| // If no panic, discard the result. |
| let _ = model |
| .route_sandbox_path( |
| &root_instance, |
| SandboxPath::used_path(&RelativePath::new("svc/hippo").unwrap()), |
| TargetDecl::Use( |
| UseBuilder::protocol() |
| .name("bar_svc") |
| .path("/svc/hippo") |
| .source(cm_rust::UseSource::Parent) |
| .build(), |
| ), |
| false, |
| ) |
| .now_or_never() |
| .expect("routing didn't finish immediately"); |
| } |
| |
| #[fuchsia::test] |
| fn config_capability_overrides() { |
| let package_value: cm_rust::ConfigValue = cm_rust::ConfigSingleValue::Uint8(1).into(); |
| let config_value: cm_rust::ConfigValue = cm_rust::ConfigSingleValue::Uint8(2).into(); |
| |
| let config = Arc::new(RuntimeConfig::default()); |
| let cm_url = make_test_url("root"); |
| |
| let decl = ComponentDeclBuilder::new() |
| .capability( |
| CapabilityBuilder::config().name("my_config").value(config_value.clone().into()), |
| ) |
| .use_( |
| UseBuilder::config() |
| .name("my_config") |
| .target_name("config") |
| .source(cm_rust::UseSource::Self_) |
| .config_type(cm_rust::ConfigValueType::Uint8), |
| ) |
| .build(); |
| |
| let mut decl_map = HashMap::<Url, (ComponentDecl, Option<ConfigFields>)>::new(); |
| decl_map.insert( |
| make_test_url("root"), |
| ( |
| decl, |
| Some(ConfigFields { |
| fields: vec![config_encoder::ConfigField { |
| key: "config".into(), |
| value: package_value, |
| mutability: Default::default(), |
| }], |
| checksum: cm_rust::ConfigChecksum::Sha256([0; 32]), |
| }), |
| ), |
| ); |
| |
| let build_model_result = ModelBuilderForAnalyzer::new(cm_url.clone()).build( |
| decl_map, |
| config, |
| Arc::new(component_id_index::Index::default()), |
| ); |
| assert_eq!(build_model_result.errors.len(), 0); |
| assert!(build_model_result.model.is_some()); |
| let model = build_model_result.model.unwrap(); |
| assert_eq!(model.len(), 1); |
| |
| let config = model.collect_config_by_url().unwrap(); |
| let config = config.get(cm_url.as_str()).unwrap(); |
| assert_eq!(config.fields.len(), 1); |
| assert_eq!(config.fields[0].key.as_str(), "config"); |
| assert_eq!(config.fields[0].value, config_value); |
| } |
| |
| // This checks that a component works successfully with just config capabilities |
| // and no Config Value File. |
| #[fuchsia::test] |
| fn config_capability_only() { |
| let config_value: cm_rust::ConfigValue = cm_rust::ConfigSingleValue::Uint8(2).into(); |
| |
| let config = Arc::new(RuntimeConfig::default()); |
| let cm_url = make_test_url("root"); |
| |
| let decl = ComponentDeclBuilder::new() |
| .capability( |
| CapabilityBuilder::config().name("my_config").value(config_value.clone().into()), |
| ) |
| .use_( |
| UseBuilder::config() |
| .name("my_config") |
| .target_name("config") |
| .source(cm_rust::UseSource::Self_) |
| .config_type(cm_rust::ConfigValueType::Uint8), |
| ) |
| .config(cm_rust::ConfigDecl { |
| fields: Box::from([]), |
| checksum: cm_rust::ConfigChecksum::Sha256([0; 32]), |
| value_source: cm_rust::ConfigValueSource::Capabilities(Default::default()), |
| }) |
| .build(); |
| |
| let mut decl_map = HashMap::<Url, (ComponentDecl, Option<ConfigFields>)>::new(); |
| decl_map.insert(make_test_url("root"), (decl, None)); |
| |
| let build_model_result = ModelBuilderForAnalyzer::new(cm_url.clone()).build( |
| decl_map, |
| config, |
| Arc::new(component_id_index::Index::default()), |
| ); |
| assert_eq!(build_model_result.errors.len(), 0); |
| assert!(build_model_result.model.is_some()); |
| let model = build_model_result.model.unwrap(); |
| assert_eq!(model.len(), 1); |
| |
| let config = model.collect_config_by_url().unwrap(); |
| let config = config.get(cm_url.as_str()).unwrap(); |
| assert_eq!(config.fields.len(), 1); |
| assert_eq!(config.fields[0].key.as_str(), "config"); |
| assert_eq!(config.fields[0].value, config_value); |
| } |
| |
| #[fuchsia::test] |
| fn config_capability_optional_from_void() { |
| let package_value: cm_rust::ConfigValue = cm_rust::ConfigSingleValue::Uint8(1).into(); |
| |
| let config = Arc::new(RuntimeConfig::default()); |
| let cm_url = make_test_url("root"); |
| |
| // Create and add the root cml. |
| let decl = ComponentDeclBuilder::new() |
| .child( |
| cm_rust_testing::ChildBuilder::new() |
| .name("child") |
| .url(&make_test_url("child").to_string()), |
| ) |
| .offer( |
| cm_rust_testing::OfferBuilder::config() |
| .source(cm_rust::offer::OfferSource::Void) |
| .name("my_config") |
| .target(cm_rust::offer::OfferTarget::Child(cm_rust::ChildRef { |
| name: "child".parse().unwrap(), |
| collection: None, |
| })) |
| .availability(cm_rust::Availability::Optional), |
| ) |
| .build(); |
| |
| let mut decl_map = HashMap::<Url, (ComponentDecl, Option<ConfigFields>)>::new(); |
| decl_map.insert(make_test_url("root"), (decl, None)); |
| |
| // Create and add the child CML. |
| let child_url = Url::new(make_test_url("child").to_string()) |
| .expect("failed to parse root component url"); |
| let decl = ComponentDeclBuilder::new() |
| .use_( |
| UseBuilder::config() |
| .name("my_config") |
| .target_name("config") |
| .source(cm_rust::UseSource::Parent) |
| .availability(cm_rust::Availability::Optional) |
| .config_type(cm_rust::ConfigValueType::Uint8), |
| ) |
| .build(); |
| |
| decl_map.insert( |
| make_test_url("child"), |
| ( |
| decl, |
| Some(ConfigFields { |
| fields: vec![config_encoder::ConfigField { |
| key: "config".into(), |
| value: package_value.clone(), |
| mutability: Default::default(), |
| }], |
| checksum: cm_rust::ConfigChecksum::Sha256([0; 32]), |
| }), |
| ), |
| ); |
| |
| let build_model_result = ModelBuilderForAnalyzer::new(cm_url).build( |
| decl_map, |
| config, |
| Arc::new(component_id_index::Index::default()), |
| ); |
| assert_eq!(build_model_result.errors.len(), 0); |
| assert!(build_model_result.model.is_some()); |
| let model = build_model_result.model.unwrap(); |
| assert_eq!(model.len(), 2); |
| |
| let config = model.collect_config_by_url().unwrap(); |
| let config = config.get(child_url.as_str()).unwrap(); |
| assert_eq!(config.fields.len(), 1); |
| assert_eq!(config.fields[0].key.as_str(), "config"); |
| assert_eq!(config.fields[0].value, package_value); |
| } |
| |
| #[fuchsia::test] |
| fn config_capability_routing_error() { |
| let config = Arc::new(RuntimeConfig::default()); |
| let cm_url = make_test_url("root"); |
| |
| let decl = ComponentDeclBuilder::new() |
| .use_( |
| UseBuilder::config() |
| .name("my_config") |
| .target_name("config") |
| .source(cm_rust::UseSource::Parent) |
| .config_type(cm_rust::ConfigValueType::Uint8), |
| ) |
| .config(cm_rust::ConfigDecl { |
| fields: Box::from([]), |
| checksum: cm_rust::ConfigChecksum::Sha256([0; 32]), |
| value_source: cm_rust::ConfigValueSource::Capabilities(Default::default()), |
| }) |
| .build(); |
| |
| let mut decl_map = HashMap::<Url, (ComponentDecl, Option<ConfigFields>)>::new(); |
| decl_map.insert(make_test_url("root"), (decl, None)); |
| |
| let build_model_result = ModelBuilderForAnalyzer::new(cm_url).build( |
| decl_map, |
| config, |
| Arc::new(component_id_index::Index::default()), |
| ); |
| assert_eq!(build_model_result.errors.len(), 0); |
| assert!(build_model_result.model.is_some()); |
| let model = build_model_result.model.unwrap(); |
| assert_eq!(model.len(), 1); |
| |
| assert_matches!(model.collect_config_by_url(), Err(_)); |
| } |
| |
| // Builds a model with structure `root -- child` in which the child environment extends the root's. |
| // Checks that the child has access to the inherited runner and resolver registrations through its |
| // environment. |
| #[fuchsia::test] |
| async fn environment_inherits() -> Result<()> { |
| let child_env_name = "child_env"; |
| let child_runner_registration = RunnerRegistration { |
| source_name: "child_env_runner".parse().unwrap(), |
| source: RegistrationSource::Self_, |
| target_name: "child_env_runner".parse().unwrap(), |
| }; |
| let child_resolver_registration = ResolverRegistration { |
| resolver: "child_env_resolver".parse().unwrap(), |
| source: RegistrationSource::Self_, |
| scheme: "child_resolver_scheme".into(), |
| }; |
| |
| let components = vec![ |
| ( |
| "root", |
| ComponentDeclBuilder::new() |
| .child(ChildBuilder::new().name("child").environment(child_env_name)) |
| .capability( |
| CapabilityBuilder::resolver() |
| .name("child_env_resolver") |
| .path("/svc/child_env_resolver"), |
| ) |
| .capability( |
| CapabilityBuilder::runner() |
| .name("child_env_runner") |
| .path("/svc/child_env_runner"), |
| ) |
| .environment( |
| EnvironmentBuilder::new() |
| .name(child_env_name) |
| .extends(fdecl::EnvironmentExtends::Realm) |
| .resolver(child_resolver_registration.clone()) |
| .runner(child_runner_registration.clone()), |
| ) |
| .build(), |
| ), |
| ("child", ComponentDeclBuilder::new().build()), |
| ]; |
| |
| // Set up the RuntimeConfig to register the `fuchsia-boot` resolver as a built-in, |
| // in addition to `builtin_runner`. |
| let mut config = RuntimeConfig::default(); |
| config.builtin_boot_resolver = component_internal::BuiltinBootResolver::Boot; |
| |
| let builtin_runner_name: Name = "builtin_elf_runner".parse().unwrap(); |
| let builtin_runner_decl = cm_rust::CapabilityDecl::Runner(cm_rust::RunnerDecl { |
| name: builtin_runner_name.clone(), |
| source_path: None, |
| }); |
| let builtin_resolver_name: Name = BOOT_SCHEME.parse().unwrap(); |
| let builtin_resolver_decl = cm_rust::CapabilityDecl::Resolver(cm_rust::ResolverDecl { |
| name: builtin_resolver_name.clone(), |
| source_path: None, |
| }); |
| config.builtin_capabilities = vec![builtin_runner_decl, builtin_resolver_decl]; |
| |
| let cm_url = make_test_url("root"); |
| let build_model_result = ModelBuilderForAnalyzer::new(cm_url).build( |
| make_decl_map(components), |
| Arc::new(config), |
| Arc::new(component_id_index::Index::default()), |
| ); |
| assert_eq!(build_model_result.errors.len(), 0); |
| assert!(build_model_result.model.is_some()); |
| let model = build_model_result.model.unwrap(); |
| assert_eq!(model.len(), 2); |
| |
| let child_instance = |
| model.get_instance(&Moniker::parse_str("child").unwrap()).expect("child instance"); |
| |
| let environment = child_instance |
| .component_sandbox() |
| .await |
| .expect("failed to get sandbox") |
| .component_input |
| .environment(); |
| let runner_router_capability = |
| environment.runners().get(&child_runner_registration.target_name).unwrap(); |
| let Capability::ConnectorRouter(runner_router) = runner_router_capability else { |
| panic!("unexpected capability for runner"); |
| }; |
| let request = runner_metadata(cm_rust::Availability::Required); |
| let source = runner_router |
| .route_debug(request, child_instance.as_weak().into()) |
| .await |
| .expect("unexpected response"); |
| assert_eq!(source.source_moniker(), Moniker::root().into()); |
| |
| let resolver_router_capability = environment |
| .resolvers() |
| .get(&Name::new(&child_resolver_registration.scheme).unwrap()) |
| .unwrap(); |
| let Capability::ConnectorRouter(resolver_router) = resolver_router_capability else { |
| panic!("unexpected capability for resolver"); |
| }; |
| let request = resolver_metadata(cm_rust::Availability::Required); |
| let source = resolver_router |
| .route_debug(request, child_instance.as_weak().into()) |
| .await |
| .expect("unexpected response"); |
| assert_eq!(source.source_moniker(), Moniker::root().into()); |
| |
| let runner_router_capability = |
| environment.runners().get(&child_runner_registration.target_name).unwrap(); |
| let Capability::ConnectorRouter(runner_router) = runner_router_capability else { |
| panic!("unexpected capability for runner"); |
| }; |
| let request = runner_metadata(cm_rust::Availability::Required); |
| let source = runner_router |
| .route_debug(request, child_instance.as_weak().into()) |
| .await |
| .expect("unexpected response"); |
| assert_eq!(source.source_moniker(), Moniker::root().into()); |
| |
| let runner_router_capability = environment.runners().get(&builtin_runner_name).unwrap(); |
| let Capability::ConnectorRouter(runner_router) = runner_router_capability else { |
| panic!("unexpected capability for runner"); |
| }; |
| let request = runner_metadata(cm_rust::Availability::Required); |
| let source = runner_router |
| .route_debug(request, child_instance.as_weak().into()) |
| .await |
| .expect("unexpected response"); |
| assert_eq!(source.source_moniker(), ExtendedMoniker::ComponentManager); |
| |
| let scheme_name = Name::new(&*BOOT_SCHEME).unwrap(); |
| let resolver_router_capability = environment.resolvers().get(&scheme_name).unwrap(); |
| let Capability::ConnectorRouter(resolver_router) = resolver_router_capability else { |
| panic!("unexpected capability for resolver"); |
| }; |
| let request = resolver_metadata(cm_rust::Availability::Required); |
| let source = resolver_router |
| .route_debug(request, child_instance.as_weak().into()) |
| .await |
| .expect("unexpected response"); |
| assert_eq!(source.source_moniker(), ExtendedMoniker::ComponentManager); |
| |
| Ok(()) |
| } |
| |
| fn decl(id: &str) -> ComponentDecl { |
| // Identify decls by a single child named `id`. |
| ComponentDeclBuilder::new().child_default(id).build() |
| } |
| |
| #[fuchsia::test] |
| fn get_decl_by_url_none() { |
| let beta_beta_urls = vec![ |
| Url::new("fuchsia-pkg://test.fuchsia.com/beta#beta.cm").unwrap(), |
| Url::new("fuchsia-pkg://test.fuchsia.com/beta/0#beta.cm").unwrap(), |
| Url::new("fuchsia-pkg://test.fuchsia.com/beta?hash=0000000000000000000000000000000000000000000000000000000000000000#beta.cm").unwrap(), |
| Url::new("fuchsia-pkg://test.fuchsia.com/beta/0?hash=0000000000000000000000000000000000000000000000000000000000000000#beta.cm").unwrap(), |
| ]; |
| let decls_by_url_no_beta_beta = hashmap! { |
| Url::new("fuchsia-pkg://test.fuchsia.com/alpha#beta.cm").unwrap() => (decl("alpha_beta"), None), |
| Url::new("fuchsia-pkg://test.fuchsia.com/beta/0#alpha.cm").unwrap() => (decl("beta_alpha"), None), |
| Url::new("fuchsia-pkg://test.fuchsia.com/gamma?hash=0000000000000000000000000000000000000000000000000000000000000000#beta.cm").unwrap() => (decl("gamma_beta"), None), |
| }; |
| |
| for beta_beta_url in beta_beta_urls.iter() { |
| let result = |
| ModelBuilderForAnalyzer::get_decl_by_url(&decls_by_url_no_beta_beta, beta_beta_url); |
| assert!(result.is_ok()); |
| assert_eq!(None, result.ok().unwrap()); |
| } |
| } |
| |
| #[fuchsia::test] |
| fn get_decl_by_url_fuchsia_boot() { |
| let fuchsia_boot_url = Url::new("fuchsia-boot:///#meta/boot.cm").unwrap(); |
| let fuchsia_boot_component = decl("boot"); |
| let decls_by_url_with_fuchsia_boot = hashmap! { |
| Url::new("fuchsia-pkg://test.fuchsia.com/alpha#beta.cm").unwrap() => (decl("alpha_beta"), None), |
| Url::new("fuchsia-pkg://test.fuchsia.com/beta/0#alpha.cm").unwrap() => (decl("beta_alpha"), None), |
| Url::new("fuchsia-pkg://test.fuchsia.com/gamma?hash=0000000000000000000000000000000000000000000000000000000000000000#beta.cm").unwrap() => (decl("gamma_beta"), None), |
| fuchsia_boot_url.clone() => (fuchsia_boot_component.clone(), None), |
| }; |
| |
| let result = ModelBuilderForAnalyzer::get_decl_by_url( |
| &decls_by_url_with_fuchsia_boot, |
| &fuchsia_boot_url, |
| ); |
| |
| assert!(result.is_ok()); |
| assert_eq!(Some(&(fuchsia_boot_component, None)), result.ok().unwrap()); |
| } |
| |
| #[fuchsia::test] |
| fn get_decl_by_url_bad_url() { |
| let bad_url = |
| Url::new("fuchsia-pkg:///test.fuchsia.com/alpha?hash=notahexvalue#meta/alpha.cm") |
| .unwrap(); |
| let empty_decls_by_url = hashmap! {}; |
| |
| let result = ModelBuilderForAnalyzer::get_decl_by_url(&empty_decls_by_url, &bad_url); |
| |
| assert!(result.is_err()); |
| } |
| |
| #[fuchsia::test] |
| fn get_decl_by_url_strong() { |
| let beta_beta_url = Url::new("fuchsia-pkg://test.fuchsia.com/beta/0?hash=0000000000000000000000000000000000000000000000000000000000000000#beta.cm").unwrap(); |
| let beta_beta_decl = decl("beta_beta"); |
| let decls_by_url_with_beta_beta = hashmap! { |
| Url::new("fuchsia-pkg://test.fuchsia.com/alpha#beta.cm").unwrap() => (decl("alpha_beta"), None), |
| Url::new("fuchsia-pkg://test.fuchsia.com/beta/0#alpha.cm").unwrap() => (decl("beta_alpha"), None), |
| Url::new("fuchsia-pkg://test.fuchsia.com/gamma?hash=0000000000000000000000000000000000000000000000000000000000000000#beta.cm").unwrap() => (decl("gamma_beta"), None), |
| beta_beta_url.clone() => (beta_beta_decl.clone(), None), |
| }; |
| |
| let result = |
| ModelBuilderForAnalyzer::get_decl_by_url(&decls_by_url_with_beta_beta, &beta_beta_url); |
| |
| assert!(result.is_ok()); |
| assert_eq!(Some(&(beta_beta_decl, None)), result.ok().unwrap()); |
| } |
| |
| #[fuchsia::test] |
| fn get_decl_by_url_strongest() { |
| let beta_beta_strong_url = Url::new("fuchsia-pkg://test.fuchsia.com/beta/0?hash=0000000000000000000000000000000000000000000000000000000000000000#beta.cm").unwrap(); |
| let beta_beta_strong_decl = decl("beta_beta_strong"); |
| let beta_beta_weak_url_1 = |
| Url::new("fuchsia-pkg://test.fuchsia.com/beta/0#beta.cm").unwrap(); |
| let beta_beta_weak_decl_1 = decl("beta_beta_weak_1"); |
| let beta_beta_weak_url_2 = Url::new("fuchsia-pkg://test.fuchsia.com/beta?hash=0000000000000000000000000000000000000000000000000000000000000000#beta.cm").unwrap(); |
| let beta_beta_weak_decl_2 = decl("beta_beta_weak_2"); |
| let beta_beta_weak_url_3 = Url::new("fuchsia-pkg://test.fuchsia.com/beta#beta.cm").unwrap(); |
| let beta_beta_weak_decl_3 = decl("beta_beta_weak_3"); |
| let decls_by_url_with_4_beta_betas = hashmap! { |
| beta_beta_weak_url_1 => (beta_beta_weak_decl_1, None), |
| beta_beta_weak_url_2 => (beta_beta_weak_decl_2, None), |
| beta_beta_weak_url_3 => (beta_beta_weak_decl_3, None), |
| beta_beta_strong_url.clone() => (beta_beta_strong_decl.clone(), None), |
| }; |
| |
| let result = ModelBuilderForAnalyzer::get_decl_by_url( |
| &decls_by_url_with_4_beta_betas, |
| &beta_beta_strong_url, |
| ); |
| |
| assert!(result.is_ok()); |
| assert_eq!(Some(&(beta_beta_strong_decl, None)), result.ok().unwrap()); |
| } |
| |
| #[fuchsia::test] |
| fn get_decl_by_url_weak() { |
| let beta_beta_strong_url = Url::new("fuchsia-pkg://test.fuchsia.com/beta/0?hash=0000000000000000000000000000000000000000000000000000000000000000#beta.cm").unwrap(); |
| let beta_beta_weak_url = Url::new("fuchsia-pkg://test.fuchsia.com/beta/0?hash=0000000000000000000000000000000000000000000000000000000000000000#beta.cm").unwrap(); |
| let beta_beta_decl = decl("beta_beta"); |
| let decls_by_url_with_strong_beta_beta = hashmap! { |
| Url::new("fuchsia-pkg://test.fuchsia.com/alpha#beta.cm").unwrap() => (decl("alpha_beta"), None), |
| Url::new("fuchsia-pkg://test.fuchsia.com/beta/0#alpha.cm").unwrap() => (decl("beta_alpha"), None), |
| Url::new("fuchsia-pkg://test.fuchsia.com/gamma?hash=0000000000000000000000000000000000000000000000000000000000000000#beta.cm").unwrap() => (decl("gamma_beta"), None), |
| beta_beta_strong_url.clone() => (beta_beta_decl.clone(), None), |
| }; |
| |
| let result = ModelBuilderForAnalyzer::get_decl_by_url( |
| &decls_by_url_with_strong_beta_beta, |
| &beta_beta_weak_url, |
| ); |
| |
| assert!(result.is_ok()); |
| assert_eq!(Some(&(beta_beta_decl, None)), result.ok().unwrap()); |
| } |
| |
| #[fuchsia::test] |
| fn get_decl_by_url_weak_any() { |
| let beta_beta_url_1 = Url::new("fuchsia-pkg://test.fuchsia.com/beta/0?hash=0000000000000000000000000000000000000000000000000000000000000000#beta.cm").unwrap(); |
| let beta_beta_decl_1 = decl("beta_beta_strong"); |
| let beta_beta_url_2 = Url::new("fuchsia-pkg://test.fuchsia.com/beta/0#beta.cm").unwrap(); |
| let beta_beta_decl_2 = decl("beta_beta_weak_1"); |
| let beta_beta_url_3 = Url::new("fuchsia-pkg://test.fuchsia.com/beta?hash=0000000000000000000000000000000000000000000000000000000000000000#beta.cm").unwrap(); |
| let beta_beta_decl_3 = decl("beta_beta_weak_2"); |
| let beta_beta_weakest_url = |
| Url::new("fuchsia-pkg://test.fuchsia.com/beta#beta.cm").unwrap(); |
| let decls_by_url_3_weak_matches = hashmap! { |
| Url::new("fuchsia-pkg://test.fuchsia.com/alpha#beta.cm").unwrap() => (decl("alpha_beta"), None), |
| Url::new("fuchsia-pkg://test.fuchsia.com/beta/0#alpha.cm").unwrap() => (decl("beta_alpha"), None), |
| Url::new("fuchsia-pkg://test.fuchsia.com/gamma?hash=0000000000000000000000000000000000000000000000000000000000000000#beta.cm").unwrap() => (decl("gamma_beta"), None), |
| beta_beta_url_1 => (beta_beta_decl_1.clone(), None), |
| beta_beta_url_2 => (beta_beta_decl_2.clone(), None), |
| beta_beta_url_3 => (beta_beta_decl_3.clone(), None), |
| }; |
| |
| let result = ModelBuilderForAnalyzer::get_decl_by_url( |
| &decls_by_url_3_weak_matches, |
| &beta_beta_weakest_url, |
| ); |
| |
| assert!(result.is_ok()); |
| let actual_decl = result.ok().unwrap().unwrap(); |
| assert!( |
| beta_beta_decl_1 == actual_decl.0 |
| || beta_beta_decl_2 == actual_decl.0 |
| || beta_beta_decl_3 == actual_decl.0 |
| ); |
| } |
| } |