blob: d0022521b99defe4748c2cf99d98b973f5b1290c [file] [log] [blame]
// 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},
match_absolute_component_urls,
route::VerifyRouteResult,
PkgUrlMatch,
},
anyhow::{anyhow, Context, Result},
bedrock_error::Explain,
cm_config::RuntimeConfig,
cm_rust::{
CapabilityDecl, CapabilityTypeName, ComponentDecl, ExposeDecl, ExposeDeclCommon, OfferDecl,
OfferDeclCommon, OfferStorageDecl, OfferTarget, ProgramDecl, ResolverRegistration,
SourceName, UseDecl, UseDeclCommon, UseEventStreamDecl, UseRunnerDecl, UseSource,
UseStorageDecl,
},
config_encoder::ConfigFields,
fidl::prelude::*,
fidl_fuchsia_sys2 as fsys,
fuchsia_url::AbsoluteComponentUrl,
fuchsia_zircon_status as zx_status,
futures::FutureExt,
moniker::{ChildName, ChildNameBase, Moniker, MonikerBase},
routing::{
capability_source::{CapabilitySource, ComponentCapability},
component_instance::{
ComponentInstanceInterface, ExtendedInstanceInterface, TopInstanceInterface,
},
environment::{
component_has_relative_url, find_first_absolute_ancestor_url, RunnerRegistry,
},
error::{ComponentInstanceError, RoutingError},
legacy_router::RouteBundle,
mapper::{RouteMapper, RouteSegment},
policy::GlobalPolicyChecker,
route_capability, route_event_stream, RouteRequest, RouteSource,
},
serde::{Deserialize, Serialize},
std::{
collections::{BTreeMap, HashMap, HashSet},
sync::Arc,
},
thiserror::Error,
url::Url,
};
/// 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(String),
#[error("the capability `{0}` is not a valid source for the capability `{1}`")]
InvalidSourceCapability(String, String),
#[error("uses Event capability `{0}` without using the EventSource protocol")]
MissingEventSourceProtocol(String),
#[error("no resolver found in environment for scheme `{0}`")]
MissingResolverForScheme(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::MissingEventSourceProtocol(_) => 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(),
}
}
}
/// 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() }
}
}
impl ModelBuilderForAnalyzer {
pub fn new(default_root_url: Url) -> Self {
Self { default_root_url }
}
fn load_dynamic_components(
input: HashMap<Moniker, (AbsoluteComponentUrl, Option<String>)>,
) -> (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 mut moniker_vec = moniker.path().clone();
let child_moniker = moniker_vec.pop();
let parent_moniker = Moniker::new(moniker_vec);
if child_moniker.is_none() {
errors.push(
BuildAnalyzerModelError::DynamicComponentInvalidMoniker(url.to_string()).into(),
);
continue;
}
let child_moniker = child_moniker.unwrap();
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::parse(&url.to_string()) {
Ok(url) => {
children.push(Child { child_moniker, 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>,
runner_registry: RunnerRegistry,
) -> BuildModelResult {
self.build_with_dynamic_components(
HashMap::new(),
decls_by_url,
runtime_config,
component_id_index,
runner_registry,
)
}
pub fn build_with_dynamic_components(
self,
dynamic_components: HashMap<Moniker, (AbsoluteComponentUrl, Option<String>)>,
decls_by_url: HashMap<Url, (ComponentDecl, Option<ConfigFields>)>,
runtime_config: Arc<RuntimeConfig>,
component_id_index: Arc<component_id_index::Index>,
runner_registry: RunnerRegistry,
) -> BuildModelResult {
let mut result = BuildModelResult::new();
let (dynamic_components, mut dynamic_component_errors) =
Self::load_dynamic_components(dynamic_components);
result.errors.append(&mut dynamic_component_errors);
// 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 = match &runtime_config.root_component_url {
None => Some(self.default_root_url.clone()),
Some(url) => match Url::parse(url.as_str()) {
Ok(url) => Some(url),
Err(err) => {
result.errors.push(anyhow!(
r#"Failed to parse root cm URL "{}" as generic URL: {:?}"#,
url.as_str(),
err
));
None
}
},
};
// If `root_url` matches a `ComponentDecl` in `decls_by_url`, construct the root
// instance and then recursively add child instances to the model.
if let Some(root_url) = root_url {
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.to_string(),
Arc::clone(&model.top_instance),
Arc::clone(&runtime_config),
model.policy_checker.clone(),
Arc::clone(&model.component_id_index),
runner_registry,
);
Self::add_descendants(
&root_instance,
&decls_by_url,
&dynamic_components,
&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>>,
model: &mut ComponentModelForAnalyzer,
result: &mut BuildModelResult,
) {
let mut children = vec![];
for child_decl in instance.decl.children.iter() {
let child_moniker = match ChildName::try_new(&child_decl.name, None) {
Ok(cm) => cm,
Err(err) => {
result.errors.push(anyhow!(err));
continue;
}
};
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
.into_iter()
.map(|dynamic_child| dynamic_child.clone())
.collect(),
);
}
for child in children.iter() {
match Self::get_absolute_child_url(&child.url.to_string(), instance) {
Ok(url) => {
let absolute_url = url;
if child.child_moniker.name().is_empty() {
result.errors.push(anyhow!(BuildAnalyzerModelError::InvalidChildDecl(
absolute_url.to_string(),
instance.moniker().to_string(),
)));
continue;
}
match Self::get_decl_by_url(decls_by_url, &absolute_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,
absolute_url.to_string(),
child_component_decl.clone(),
child_config.clone(),
Arc::clone(instance),
model.policy_checker.clone(),
Arc::clone(&model.component_id_index),
) {
Ok(child_instance) => {
Self::add_descendants(
&child_instance,
decls_by_url,
dynamic_components,
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(
absolute_url.to_string(),
instance.moniker().to_string(),
)
));
}
}
}
Err(err) => {
result.errors.push(anyhow!(err));
}
}
}
}
// 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: &str,
instance: &Arc<ComponentInstanceForAnalyzer>,
) -> Result<Url, BuildAnalyzerModelError> {
let err = BuildAnalyzerModelError::MalformedUrl(
instance.url().to_string(),
instance.moniker().to_string(),
);
match Url::parse(child_url) {
Ok(url) => Ok(url),
Err(url::ParseError::RelativeUrlWithoutBase) => {
let absolute_prefix = match component_has_relative_url(instance) {
true => find_first_absolute_ancestor_url(instance).map_err(|_| err),
false => Url::parse(instance.url()).map_err(|_| err),
}?;
Ok(absolute_prefix
.join(child_url)
.expect("failed to join child URL to absolute prefix"))
}
_ => Err(err),
}
}
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() != "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() != "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 {
tracing::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 {
tracing::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 {
tracing::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]));
}
}
}
}
/// `ComponentModelForAnalyzer` owns a representation of the v2 component graph and
/// supports lookup of component instances by `Moniker`.
#[derive(Debug, Default)]
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.clone().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::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.
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(_) => {
// TODO(https://fxbug.dev/301674053): Support dictionary routing.
false
}
};
if should_check_offer {
self.check_offer_capability(offer_decl, target)
} else {
// This offer decl doesn't need to be checked.
vec![]
}
}
pub fn check_offer_capability(
self: &Arc<Self>,
offer_decl: &OfferDecl,
target: &Arc<ComponentInstanceForAnalyzer>,
) -> Vec<VerifyRouteResult> {
let mut results = Vec::new();
let (capability, route_request) = match offer_decl.clone() {
OfferDecl::Protocol(offer_decl) => {
let capability = offer_decl.source_name.clone();
let route_request = RouteRequest::OfferProtocol(offer_decl);
(capability, route_request)
}
OfferDecl::Directory(offer_decl) => {
let capability = offer_decl.source_name.clone();
let route_request = RouteRequest::OfferDirectory(offer_decl);
(capability, route_request)
}
OfferDecl::Service(offer_decl) => {
let capability = offer_decl.source_name.clone();
let route_request = RouteRequest::OfferService(RouteBundle::from_offer(offer_decl));
(capability, route_request)
}
OfferDecl::EventStream(offer_decl) => {
let capability = offer_decl.source_name.clone();
let route_request = RouteRequest::OfferEventStream(offer_decl);
(capability, route_request)
}
OfferDecl::Runner(offer_decl) => {
let capability = offer_decl.source_name.clone();
let route_request = RouteRequest::OfferRunner(offer_decl);
(capability, route_request)
}
OfferDecl::Resolver(offer_decl) => {
let capability = offer_decl.source_name.clone();
let route_request = RouteRequest::OfferResolver(offer_decl);
(capability, route_request)
}
OfferDecl::Config(offer_decl) => {
let capability = offer_decl.source_name.clone();
let route_request = RouteRequest::OfferConfig(offer_decl);
(capability, route_request)
}
// Storage capabilities are a special case because they result in 2 routes.
OfferDecl::Storage(offer_decl) => {
let capability = offer_decl.source_name.clone();
let (result, storage_route, dir_route) =
Self::route_storage_and_backing_directory_from_offer_sync(offer_decl, target);
// Ignore any valid routes to void.
if let Ok(ref source) = result {
if matches!(source.source, CapabilitySource::Void { .. }) {
return vec![];
}
}
match (
result.map_err(|e| AnalyzerModelError::from(e)),
vec![storage_route, dir_route],
capability,
) {
(Ok(source), routes, capability) => match self.check_use_source(&source) {
Ok(()) => {
for route in routes.into_iter() {
results.push(VerifyRouteResult {
using_node: target.moniker().clone(),
capability: Some(capability.clone()),
error: None,
route,
});
}
}
Err(err) => {
for route in routes.into_iter() {
results.push(VerifyRouteResult {
using_node: target.moniker().clone(),
capability: Some(capability.clone()),
error: Some(err.clone()),
route,
});
}
}
},
(Err(err), routes, capability) => {
for route in routes.into_iter() {
results.push(VerifyRouteResult {
using_node: target.moniker().clone(),
capability: Some(capability.clone()),
error: Some(err.clone()),
route,
});
}
}
}
return results;
}
OfferDecl::Dictionary(_offer_decl) => {
// TODO(https://fxbug.dev/301674053): Support this.
return vec![];
}
};
let (route_result, route) = Self::route_capability_sync(route_request, target);
let source = match route_result {
Ok(source) => source,
Err(err) => {
results.push(VerifyRouteResult {
using_node: target.moniker().clone(),
capability: Some(capability.clone()),
error: Some(err.into()),
route,
});
return results;
}
};
// Ignore any valid routes to void.
if let CapabilitySource::Void { .. } = source.source {
return vec![];
}
match self.check_use_source(&source) {
Ok(()) => {
results.push(VerifyRouteResult {
using_node: target.moniker().clone(),
capability: Some(capability.clone()),
error: None,
route,
});
}
Err(err) => {
results.push(VerifyRouteResult {
using_node: target.moniker().clone(),
capability: Some(capability.clone()),
error: Some(err),
route,
});
}
};
results
}
/// Checks the routing for all capabilities of the specified types that are `used` by `target`.
pub 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) {
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) {
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) {
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));
}
results
}
/// 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 fn check_use_capability(
self: &Arc<Self>,
use_decl: &UseDecl,
target: &Arc<ComponentInstanceForAnalyzer>,
) -> Vec<VerifyRouteResult> {
let mut results = Vec::new();
let route_result = match use_decl.clone() {
UseDecl::Directory(use_directory_decl) => {
let capability = use_directory_decl.source_name.clone();
let (result, route) = Self::route_capability_sync(
RouteRequest::UseDirectory(use_directory_decl),
target,
);
// Ignore any valid routes to void.
if let Ok(ref source) = result {
if matches!(source.source, CapabilitySource::Void { .. }) {
return vec![];
}
}
(result.map_err(|e| AnalyzerModelError::from(e)), vec![route], capability)
}
UseDecl::Protocol(use_protocol_decl) => {
let capability = use_protocol_decl.source_name.clone();
let (result, route) = Self::route_capability_sync(
RouteRequest::UseProtocol(use_protocol_decl),
target,
);
// Ignore any valid routes to void.
if let Ok(ref source) = result {
if matches!(source.source, CapabilitySource::Void { .. }) {
return vec![];
}
}
(result.map_err(|e| e.into()), vec![route], capability)
}
UseDecl::Service(use_service_decl) => {
let capability = use_service_decl.source_name.clone();
let (result, route) = Self::route_capability_sync(
RouteRequest::UseService(use_service_decl.clone()),
target,
);
// Ignore any valid routes to void.
if let Ok(ref source) = result {
if matches!(source.source, CapabilitySource::Void { .. }) {
return vec![];
}
}
(result.map_err(|e| e.into()), vec![route], capability)
}
UseDecl::Storage(use_storage_decl) => {
let capability = use_storage_decl.source_name.clone();
let (result, storage_route, dir_route) =
Self::route_storage_and_backing_directory_sync(use_storage_decl, target);
// Ignore any valid routes to void.
if let Ok(ref source) = result {
if matches!(source.source, CapabilitySource::Void { .. }) {
return vec![];
}
}
let result = result.map_err(|e| e.into());
(result, vec![storage_route, dir_route], capability)
}
UseDecl::EventStream(use_event_stream_decl) => {
let capability = use_event_stream_decl.source_name.clone();
match Self::route_capability_sync(
RouteRequest::UseEventStream(use_event_stream_decl),
target,
) {
(Ok(source), route) => (Ok(source), vec![route], capability),
(Err(err), route) => (Err(err.into()), vec![route], capability),
}
}
UseDecl::Runner(use_runner_decl) => {
let capability = use_runner_decl.source_name.clone();
match Self::route_capability_sync(RouteRequest::UseRunner(use_runner_decl), target)
{
(Ok(source), route) => (Ok(source), vec![route], capability),
(Err(err), route) => (Err(err.into()), vec![route], capability),
}
}
UseDecl::Config(use_config_decl) => {
let capability = use_config_decl.source_name.clone();
match Self::route_capability_sync(RouteRequest::UseConfig(use_config_decl), target)
{
(Ok(source), route) => (Ok(source), vec![route], capability),
(Err(err), route) => (Err(err.into()), vec![route], capability),
}
}
};
match route_result {
(Ok(source), routes, capability) => match self.check_use_source(&source) {
Ok(()) => {
for route in routes.into_iter() {
results.push(VerifyRouteResult {
using_node: target.moniker().clone(),
capability: Some(capability.clone()),
error: None,
route,
});
}
}
Err(err) => {
for route in routes.into_iter() {
results.push(VerifyRouteResult {
using_node: target.moniker().clone(),
capability: Some(capability.clone()),
error: Some(err.clone()),
route,
});
}
}
},
(Err(err), routes, capability) => {
for route in routes.into_iter() {
results.push(VerifyRouteResult {
using_node: target.moniker().clone(),
capability: Some(capability.clone()),
error: Some(err.clone()),
route,
});
}
}
}
results
}
/// 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 fn check_use_exposed_capability(
self: &Arc<Self>,
expose_decl: &ExposeDecl,
target: &Arc<ComponentInstanceForAnalyzer>,
) -> Option<VerifyRouteResult> {
match self.request_from_expose(expose_decl) {
Some(request) => {
let (result, route) = Self::route_capability_sync(request, target);
let error =
result.map_err(|e| e.into()).and_then(|s| self.check_use_source(&s)).err();
Some(VerifyRouteResult {
using_node: target.moniker().clone(),
capability: Some(expose_decl.target_name().clone()),
error,
route,
})
}
None => None,
}
}
/// 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 (result, route) = Self::route_capability_sync(
RouteRequest::UseRunner(UseRunnerDecl {
source: UseSource::Environment,
source_name: runner.clone(),
source_dictionary: Default::default(),
}),
target,
);
match result {
Ok(_source) => Some(VerifyRouteResult {
using_node: target.moniker().clone(),
capability: Some(runner.clone()),
error: None,
route,
}),
Err(err) => Some(VerifyRouteResult {
using_node: target.moniker().clone(),
capability: Some(runner.clone()),
error: Some(err.into()),
route,
}),
}
}
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 fn check_resolver(
self: &Arc<Self>,
target: &Arc<ComponentInstanceForAnalyzer>,
) -> VerifyRouteResult {
let url = Url::parse(target.url()).expect("failed to parse target URL");
let scheme = url.scheme();
match target.environment.get_registered_resolver(scheme) {
Ok(Some((ExtendedInstanceInterface::Component(instance), resolver))) => {
let (route_result, route) = Self::route_capability_sync(
RouteRequest::Resolver(resolver.clone()),
&instance,
);
match route_result {
Ok(_source) => VerifyRouteResult {
using_node: target.moniker().clone(),
capability: Some(resolver.resolver),
error: None,
route,
},
Err(err) => VerifyRouteResult {
using_node: target.moniker().clone(),
capability: Some(resolver.resolver),
error: Some(err.into()),
route,
},
}
}
Ok(Some((ExtendedInstanceInterface::AboveRoot(_), resolver))) => {
match self.get_builtin_resolver_decl(&resolver) {
Ok(decl) => {
let route = vec![RouteSegment::ProvideAsBuiltin { capability: decl }];
VerifyRouteResult {
using_node: target.moniker().clone(),
capability: Some(resolver.resolver),
error: None,
route,
}
}
Err(err) => VerifyRouteResult {
using_node: target.moniker().clone(),
capability: Some(resolver.resolver),
error: Some(err),
route: vec![],
},
}
}
Ok(None) => VerifyRouteResult {
using_node: target.moniker().clone(),
capability: None,
error: Some(AnalyzerModelError::MissingResolverForScheme(scheme.to_string())),
route: vec![],
},
Err(err) => VerifyRouteResult {
using_node: target.moniker().clone(),
capability: None,
error: Some(AnalyzerModelError::from(err)),
route: vec![],
},
}
}
// Retrieves the `CapabilityDecl` for a built-in resolver from its registration, or an
// error if the resolver is not provided as a built-in capability.
fn get_builtin_resolver_decl(
&self,
resolver: &ResolverRegistration,
) -> Result<CapabilityDecl, AnalyzerModelError> {
match self.top_instance.builtin_capabilities().iter().find(|&decl| {
if let CapabilityDecl::Resolver(resolver_decl) = decl {
resolver_decl.name == resolver.resolver
} else {
false
}
}) {
Some(decl) => Ok(decl.clone()),
None => Err(AnalyzerModelError::RoutingError(
RoutingError::use_from_component_manager_not_found(resolver.resolver.to_string()),
)),
}
}
// Checks properties of a capability source that are necessary to use the capability
// and that are possible to verify statically.
fn check_use_source(
&self,
route_source: &RouteSource<ComponentInstanceForAnalyzer>,
) -> Result<(), AnalyzerModelError> {
match &route_source.source {
CapabilitySource::Component { component: weak, .. } => {
self.check_executable(&weak.upgrade()?)
}
CapabilitySource::Namespace { .. } => Ok(()),
CapabilitySource::Capability { source_capability, component: weak } => {
self.check_capability_source(&weak.upgrade()?, &source_capability)
}
CapabilitySource::Builtin { .. } => Ok(()),
CapabilitySource::Framework { .. } => Ok(()),
CapabilitySource::Void { .. } => Ok(()),
_ => unimplemented![],
}
}
// A helper function validating a source of type `Capability`.
// The source should be a `StorageAdmin` protocol since that is only supported capability
// source, and it should route to a valid storage source.
fn check_capability_source(
&self,
source_component: &Arc<ComponentInstanceForAnalyzer>,
source_capability: &ComponentCapability,
) -> Result<(), AnalyzerModelError> {
let source_capability_name = source_capability
.source_capability_name()
.expect("failed to get source capability name");
match source_capability.source_name().map(|name| name.to_string()).as_deref() {
Some(fsys::StorageAdminMarker::PROTOCOL_NAME) => {
match source_component.decl.find_storage_source(source_capability_name) {
Some(_) => Ok(()),
None => Err(AnalyzerModelError::InvalidSourceCapability(
source_capability_name.to_string(),
fsys::StorageAdminMarker::PROTOCOL_NAME.to_string(),
)),
}
}
_ => Err(AnalyzerModelError::InvalidSourceCapability(
source_capability_name.to_string(),
source_capability
.source_name()
.map_or_else(|| "".to_string(), |name| name.to_string()),
)),
}
}
// A helper function which prepares a route request for capabilities which can be used
// from an expose declaration, and returns None if the capability type cannot be used
// from an expose.
fn request_from_expose(self: &Arc<Self>, expose_decl: &ExposeDecl) -> Option<RouteRequest> {
match expose_decl {
ExposeDecl::Directory(expose_directory_decl) => {
Some(RouteRequest::ExposeDirectory(expose_directory_decl.clone()))
}
ExposeDecl::Protocol(expose_protocol_decl) => {
Some(RouteRequest::ExposeProtocol(expose_protocol_decl.clone()))
}
ExposeDecl::Service(expose_service_decl) => Some(RouteRequest::ExposeService(
RouteBundle::from_expose(expose_service_decl.clone()),
)),
_ => None,
}
}
// 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().to_string(),
)),
}
}
// Routes a capability from a `ComponentInstanceForAnalyzer` and panics if the future returned by
// `route_capability` is not ready immediately.
//
// TODO(https://fxbug.dev/42168300): Remove this function and use `route_capability` directly when Scrutiny's
// `DataController`s allow async function calls.
pub fn route_capability_sync(
request: RouteRequest,
target: &Arc<ComponentInstanceForAnalyzer>,
) -> (Result<RouteSource<ComponentInstanceForAnalyzer>, RoutingError>, Vec<RouteSegment>) {
let mut mapper = RouteMapper::new();
let result = route_capability(request, target, &mut mapper)
.now_or_never()
.expect("future was not ready immediately");
(result, mapper.get_route())
}
pub fn route_event_stream_sync(
request: UseEventStreamDecl,
target: &Arc<ComponentInstanceForAnalyzer>,
) -> (Result<RouteSource<ComponentInstanceForAnalyzer>, RoutingError>, Vec<RouteSegment>) {
let mut mapper = RouteMapper::new();
let result = route_event_stream(request, target, &mut mapper)
.now_or_never()
.expect("future was not ready immediately");
(result, mapper.get_route())
}
// Routes a storage capability and its backing directory from a `ComponentInstanceForAnalyzer` and
// panics if the returned future is not ready immediately. If routing was successful, then `result`
// contains the source of the backing directory capability.
//
// TODO(https://fxbug.dev/42168300): Remove this function and use `route_capability` directly when Scrutiny's
// `DataController`s allow async function calls.
fn route_storage_and_backing_directory_sync(
use_decl: UseStorageDecl,
target: &Arc<ComponentInstanceForAnalyzer>,
) -> (
Result<RouteSource<ComponentInstanceForAnalyzer>, RoutingError>,
Vec<RouteSegment>,
Vec<RouteSegment>,
) {
let mut storage_mapper = RouteMapper::new();
let mut backing_dir_mapper = RouteMapper::new();
let result = async {
let result =
route_capability(RouteRequest::UseStorage(use_decl), target, &mut storage_mapper)
.await?;
let (storage_decl, storage_component) = match result.source {
CapabilitySource::Component {
capability: ComponentCapability::Storage(storage_decl),
component,
..
} => (storage_decl, component.upgrade()?),
CapabilitySource::Void { .. } => return Ok(result),
_ => unreachable!("unexpected storage source"),
};
route_capability(
RouteRequest::StorageBackingDirectory(storage_decl),
&storage_component,
&mut backing_dir_mapper,
)
.await
}
.now_or_never()
.expect("future was not ready immediately");
(result, storage_mapper.get_route(), backing_dir_mapper.get_route())
}
// Routes a storage capability and its backing directory from an offer to a `ComponentInstanceForAnalyzer`
// and panics if the returned future is not ready immediately. If routing was successful, then `result`
// contains the source of the backing directory capability.
//
// TODO(https://fxbug.dev/42168300): Remove this function and use `route_capability` directly when Scrutiny's
// `DataController`s allow async function calls.
fn route_storage_and_backing_directory_from_offer_sync(
offer_decl: OfferStorageDecl,
target: &Arc<ComponentInstanceForAnalyzer>,
) -> (
Result<RouteSource<ComponentInstanceForAnalyzer>, RoutingError>,
Vec<RouteSegment>,
Vec<RouteSegment>,
) {
let mut storage_mapper = RouteMapper::new();
let mut backing_dir_mapper = RouteMapper::new();
let result = async {
let result = route_capability(
RouteRequest::OfferStorage(offer_decl),
target,
&mut storage_mapper,
)
.await?;
let (storage_decl, storage_component) = match result.source {
CapabilitySource::Component {
capability: ComponentCapability::Storage(storage_decl),
component,
..
} => (storage_decl, component.upgrade()?),
CapabilitySource::Void { .. } => return Ok(result),
_ => unreachable!("unexpected storage source"),
};
route_capability(
RouteRequest::StorageBackingDirectory(storage_decl),
&storage_component,
&mut backing_dir_mapper,
)
.await
}
.now_or_never()
.expect("future was not ready immediately");
(result, storage_mapper.get_route(), backing_dir_mapper.get_route())
}
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 = routing::config::route_config_value(config, instance)
.now_or_never()
.expect("future was not ready immediately")?;
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)
}
}
#[derive(Clone)]
pub struct Child {
pub child_moniker: ChildName,
pub url: Url,
pub environment: Option<String>,
}
#[cfg(test)]
mod tests {
use {
super::ModelBuilderForAnalyzer,
crate::{environment::BOOT_SCHEME, ComponentModelForAnalyzer},
anyhow::Result,
assert_matches::assert_matches,
cm_config::RuntimeConfig,
cm_moniker::InstancedMoniker,
cm_rust::{
Availability, ComponentDecl, DependencyType, RegistrationSource, ResolverRegistration,
RunnerRegistration, UseProtocolDecl, UseSource, UseStorageDecl,
},
cm_rust_testing::{
CapabilityBuilder, ChildBuilder, ComponentDeclBuilder, EnvironmentBuilder, UseBuilder,
},
cm_types::Name,
config_encoder::ConfigFields,
fidl_fuchsia_component_internal as component_internal,
maplit::hashmap,
moniker::{ChildName, Moniker, MonikerBase},
routing::{
component_instance::{
ComponentInstanceInterface, ExtendedInstanceInterface,
WeakExtendedInstanceInterface,
},
environment::RunnerRegistry,
error::ComponentInstanceError,
RouteRequest,
},
std::{collections::HashMap, sync::Arc},
url::Url,
};
const TEST_URL_PREFIX: &str = "test:///";
fn make_test_url(component_name: &str) -> Url {
Url::parse(&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()),
RunnerRegistry::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()
);
// Include tests for `.instanced_moniker()` alongside `.moniker()`
// until`.instanced_moniker()` is removed from the public API.
assert_eq!(root_instance.moniker(), &Moniker::root());
assert_eq!(root_instance.instanced_moniker(), &InstancedMoniker::root());
assert_eq!(child_instance.moniker(), &Moniker::parse_str("child").unwrap());
assert_eq!(
child_instance.instanced_moniker(),
&InstancedMoniker::parse_str("child:0").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());
assert_eq!(component.instanced_moniker(), root_instance.instanced_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_eq!(get_child.unwrap().instanced_moniker(), child_instance.instanced_moniker());
let root_environment = root_instance.environment();
let child_environment = child_instance.environment();
assert_eq!(root_environment.env().name(), None);
match root_environment.env().parent() {
WeakExtendedInstanceInterface::AboveRoot(_) => {}
_ => panic!("root environment's parent should be `AboveRoot`"),
}
assert_eq!(child_environment.env().name(), None);
match child_environment.env().parent() {
WeakExtendedInstanceInterface::Component(component) => {
assert_eq!(component.upgrade()?.moniker(), root_instance.moniker());
assert_eq!(
component.upgrade()?.instanced_moniker(),
root_instance.instanced_moniker()
)
}
_ => panic!("child environment's parent should be root component"),
}
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::parse(&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()),
RunnerRegistry::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.as_str());
}
// 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()),
RunnerRegistry::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_capability` was not ready immediately.
// If no panic, discard the result.
let _ = ComponentModelForAnalyzer::route_capability_sync(
RouteRequest::UseProtocol(UseProtocolDecl {
source: UseSource::Parent,
source_name: "bar_svc".parse().unwrap(),
source_dictionary: Default::default(),
target_path: "/svc/hippo".parse().unwrap(),
dependency_type: DependencyType::Strong,
availability: Availability::Required,
}),
&root_instance,
);
}
// 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_storage_and_backing_directory_is_sync() {
let components = vec![("root", ComponentDeclBuilder::new().build())];
let config = Arc::new(RuntimeConfig::default());
let cm_url = cm_types::Url::new(make_test_url("root").to_string())
.expect("failed to parse root component url");
let url = Url::parse(cm_url.as_str()).expect("failed to parse root component url");
let build_model_result = ModelBuilderForAnalyzer::new(url).build(
make_decl_map(components),
config,
Arc::new(component_id_index::Index::default()),
RunnerRegistry::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_storage_and_backing_directory` was not ready immediately.
// If no panic, discard the result.
let _ = ComponentModelForAnalyzer::route_storage_and_backing_directory_sync(
UseStorageDecl {
source_name: "cache".parse().unwrap(),
target_path: "/storage".parse().unwrap(),
availability: Availability::Required,
},
&root_instance,
);
}
#[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 = cm_types::Url::new(make_test_url("root").to_string())
.expect("failed to parse root component url");
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 url = Url::parse(cm_url.as_str()).expect("failed to parse root component url");
let build_model_result = ModelBuilderForAnalyzer::new(url).build(
decl_map,
config,
Arc::new(component_id_index::Index::default()),
RunnerRegistry::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 = cm_types::Url::new(make_test_url("root").to_string())
.expect("failed to parse root component url");
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: Vec::new(),
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 url = Url::parse(cm_url.as_str()).expect("failed to parse root component url");
let build_model_result = ModelBuilderForAnalyzer::new(url).build(
decl_map,
config,
Arc::new(component_id_index::Index::default()),
RunnerRegistry::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 = cm_types::Url::new(make_test_url("root").to_string())
.expect("failed to parse root component url");
// 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::OfferSource::Void)
.name("my_config")
.target(cm_rust::OfferTarget::Child(cm_rust::ChildRef {
name: "child".into(),
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 = cm_types::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 url = Url::parse(cm_url.as_str()).expect("failed to parse root component url");
let build_model_result = ModelBuilderForAnalyzer::new(url).build(
decl_map,
config,
Arc::new(component_id_index::Index::default()),
RunnerRegistry::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 = cm_types::Url::new(make_test_url("root").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)
.config_type(cm_rust::ConfigValueType::Uint8),
)
.config(cm_rust::ConfigDecl {
fields: Vec::new(),
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 url = Url::parse(cm_url.as_str()).expect("failed to parse root component url");
let build_model_result = ModelBuilderForAnalyzer::new(url).build(
decl_map,
config,
Arc::new(component_id_index::Index::default()),
RunnerRegistry::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]
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))
.environment(
EnvironmentBuilder::new()
.name(child_env_name)
.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_runner".parse().unwrap();
let builtin_runner_registration = RunnerRegistration {
source_name: builtin_runner_name.clone(),
source: RegistrationSource::Self_,
target_name: builtin_runner_name.clone(),
};
let cm_url = cm_types::Url::new(make_test_url("root").to_string())
.expect("failed to parse root component url");
let url = Url::parse(cm_url.as_str()).expect("failed to parse root component url");
let build_model_result = ModelBuilderForAnalyzer::new(url).build(
make_decl_map(components),
Arc::new(config),
Arc::new(component_id_index::Index::default()),
RunnerRegistry::from_decl(&vec![builtin_runner_registration]),
);
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 get_child_runner_result = child_instance
.environment()
.env()
.get_registered_runner(&child_runner_registration.target_name)?;
assert!(get_child_runner_result.is_some());
let (child_runner_registrar, child_runner) = get_child_runner_result.unwrap();
match child_runner_registrar {
ExtendedInstanceInterface::Component(instance) => {
assert_eq!(instance.moniker(), &Moniker::root());
}
ExtendedInstanceInterface::AboveRoot(_) => {
panic!("expected child_env_runner to be registered by the root instance")
}
}
assert_eq!(child_runner_registration, child_runner);
let get_child_resolver_result = child_instance
.environment
.get_registered_resolver(&child_resolver_registration.scheme)?;
assert!(get_child_resolver_result.is_some());
let (child_resolver_registrar, child_resolver) = get_child_resolver_result.unwrap();
match child_resolver_registrar {
ExtendedInstanceInterface::Component(instance) => {
assert_eq!(instance.moniker(), &Moniker::root());
}
ExtendedInstanceInterface::AboveRoot(_) => {
panic!("expected child_env_resolver to be registered by the root instance")
}
}
assert_eq!(child_resolver_registration, child_resolver);
let get_builtin_runner_result =
child_instance.environment().env().get_registered_runner(&builtin_runner_name)?;
assert!(get_builtin_runner_result.is_some());
let (builtin_runner_registrar, _builtin_runner) = get_builtin_runner_result.unwrap();
match builtin_runner_registrar {
ExtendedInstanceInterface::Component(_) => {
panic!("expected builtin runner to be registered above the root")
}
ExtendedInstanceInterface::AboveRoot(_) => {}
}
let get_builtin_resolver_result =
child_instance.environment.get_registered_resolver(&BOOT_SCHEME.to_string())?;
assert!(get_builtin_resolver_result.is_some());
let (builtin_resolver_registrar, _builtin_resolver) = get_builtin_resolver_result.unwrap();
match builtin_resolver_registrar {
ExtendedInstanceInterface::Component(_) => {
panic!("expected boot resolver to be registered above the root")
}
ExtendedInstanceInterface::AboveRoot(_) => {}
}
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::parse("fuchsia-pkg://test.fuchsia.com/beta#beta.cm").unwrap(),
Url::parse("fuchsia-pkg://test.fuchsia.com/beta/0#beta.cm").unwrap(),
Url::parse("fuchsia-pkg://test.fuchsia.com/beta?hash=0000000000000000000000000000000000000000000000000000000000000000#beta.cm").unwrap(),
Url::parse("fuchsia-pkg://test.fuchsia.com/beta/0?hash=0000000000000000000000000000000000000000000000000000000000000000#beta.cm").unwrap(),
];
let decls_by_url_no_beta_beta = hashmap! {
Url::parse("fuchsia-pkg://test.fuchsia.com/alpha#beta.cm").unwrap() => (decl("alpha_beta"), None),
Url::parse("fuchsia-pkg://test.fuchsia.com/beta/0#alpha.cm").unwrap() => (decl("beta_alpha"), None),
Url::parse("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::parse("fuchsia-boot:///#meta/boot.cm").unwrap();
let fuchsia_boot_component = decl("boot");
let decls_by_url_with_fuchsia_boot = hashmap! {
Url::parse("fuchsia-pkg://test.fuchsia.com/alpha#beta.cm").unwrap() => (decl("alpha_beta"), None),
Url::parse("fuchsia-pkg://test.fuchsia.com/beta/0#alpha.cm").unwrap() => (decl("beta_alpha"), None),
Url::parse("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::parse("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::parse("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::parse("fuchsia-pkg://test.fuchsia.com/alpha#beta.cm").unwrap() => (decl("alpha_beta"), None),
Url::parse("fuchsia-pkg://test.fuchsia.com/beta/0#alpha.cm").unwrap() => (decl("beta_alpha"), None),
Url::parse("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::parse("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::parse("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::parse("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::parse("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::parse("fuchsia-pkg://test.fuchsia.com/beta/0?hash=0000000000000000000000000000000000000000000000000000000000000000#beta.cm").unwrap();
let beta_beta_weak_url = Url::parse("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::parse("fuchsia-pkg://test.fuchsia.com/alpha#beta.cm").unwrap() => (decl("alpha_beta"), None),
Url::parse("fuchsia-pkg://test.fuchsia.com/beta/0#alpha.cm").unwrap() => (decl("beta_alpha"), None),
Url::parse("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::parse("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::parse("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::parse("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::parse("fuchsia-pkg://test.fuchsia.com/beta#beta.cm").unwrap();
let decls_by_url_3_weak_matches = hashmap! {
Url::parse("fuchsia-pkg://test.fuchsia.com/alpha#beta.cm").unwrap() => (decl("alpha_beta"), None),
Url::parse("fuchsia-pkg://test.fuchsia.com/beta/0#alpha.cm").unwrap() => (decl("beta_alpha"), None),
Url::parse("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
);
}
}