blob: c3e43f2a4b42b3292d4974ec874c55304d6f58e0 [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::{
core::collection::{Component, ComponentSource, Components},
verify::collection::V2ComponentModel,
},
anyhow::{anyhow, Context, Error, Result},
cm_fidl_analyzer::{
component_model::ComponentModelForAnalyzer,
node_path::NodePath,
route::{CapabilityRouteError, RouteSegment, VerifyRouteResult},
},
cm_rust::{
CapabilityDecl, CapabilityName, CapabilityPath, CapabilityTypeName, ComponentDecl,
ExposeDecl, OfferDecl, UseDecl,
},
routing::component_instance::ComponentInstanceInterface,
scrutiny::model::{controller::DataController, model::DataModel},
serde::{Deserialize, Serialize},
serde_json::{self, json, value::Value},
serde_json5,
std::{
collections::{HashMap, HashSet},
fmt,
fs::read_to_string,
path::PathBuf,
str::FromStr,
sync::Arc,
},
thiserror::Error as ThisError,
url::Url,
};
const BAD_REQUEST_CTX: &str = "Failed to parse RouteSourcesController request";
const MISSING_TARGET_INSTANCE: &str = "Target instance is missing from component model";
const GATHER_FAILED: &str = "Target instance failed to gather routes_to_skip and routes_to_verify";
const ROUTE_LISTS_INCOMPLETE: &str = "Component route skip list + verify list incomplete";
const ROUTE_LISTS_OVERLAP: &str = "Component route skip list + verify list contains duplicates";
const MATCH_ONE_FAILED: &str = "Failed to match exactly one item";
const USE_SPEC_NAME: &str = "use spec";
const USE_DECL_NAME: &str = "use declaration";
#[derive(Deserialize, Serialize)]
pub struct RouteSourcesRequest {
// The input path to a json5 file containing `RouteSourcesConfig`.
pub input: String,
}
/// Content of a request to a `RouteSourcesController`, containing an exhuastive
/// list of routes to zero or more component instances.
#[derive(Deserialize, Serialize)]
pub struct RouteSourcesConfig {
pub component_routes: Vec<RouteSourcesSpec>,
}
/// Specification of all routes to a component instance in the component tree.
/// Each route must be listed, either to be verified or skipped by the verifier.
#[derive(Deserialize, Serialize)]
pub struct RouteSourcesSpec {
/// Absolute path to the component instance whose routes are to be verified.
pub target_node_path: NodePath,
/// Routes that are expected to be present, but do not require verification.
pub routes_to_skip: Vec<UseSpec>,
/// Route specification and route source matching information for routes
/// that are to be verified.
pub routes_to_verify: Vec<RouteMatch>,
}
/// Input query type for matching routes by target. Usually, either a `path` or
/// a `name` is specified.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct UseSpec {
/// Route capability types.
#[serde(rename = "use_type")]
pub type_name: CapabilityTypeName,
/// Target capability path the match, if any.
#[serde(rename = "use_path")]
pub path: Option<CapabilityPath>,
/// Target capability name, if any.
#[serde(rename = "use_name")]
pub name: Option<CapabilityName>,
}
/// Match a `UseDecl` to a `UseSpec` when types match and spec'd `name` and/or
/// `path` matches.
impl Matches<UseDecl> for UseSpec {
const NAME: &'static str = USE_SPEC_NAME;
const OTHER_NAME: &'static str = USE_DECL_NAME;
fn matches(&self, other: &UseDecl) -> Result<bool> {
Ok(self.type_name == other.into()
&& (self.path.is_none() || self.path.as_ref() == other.path())
&& (self.name.is_none() || self.name.as_ref() == other.name()))
}
}
/// Input query type for matching routes by target and source.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct RouteMatch {
/// Route information to match an entry in the component instance `uses`
/// array.
#[serde(flatten)]
target: UseSpec,
/// Route source information to match the capability declaration at the
/// route's source.
#[serde(flatten)]
source: SourceSpec,
}
/// Input query type for matching a route source.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct SourceSpec {
/// Node path prefix expected at the source instance.
#[serde(rename = "source_node_path")]
node_path: NodePath,
/// Capability declaration expected at the source instance.
#[serde(flatten)]
capability: SourceDeclSpec,
}
/// Input query type for matching a capability declaration. Usually, either a
// `path` or a `name` is specified.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct SourceDeclSpec {
/// Path designated in capability declaration, if any.
#[serde(rename = "source_path_prefix")]
pub path_prefix: Option<CapabilityPath>,
/// Name designated in capability declaration, if any.
#[serde(rename = "source_name")]
pub name: Option<CapabilityName>,
}
/// Match a `CapabilityDecl` to a `SourceDeclSpec`. Only the `name` is matched because matching
/// `path_prefix` requires complete capability route.
impl Matches<CapabilityDecl> for SourceDeclSpec {
const NAME: &'static str = "source declaration spec";
const OTHER_NAME: &'static str = "capability declaration";
fn matches(&self, other: &CapabilityDecl) -> Result<bool> {
Ok(match &self.name {
Some(name) => name == other.name(),
None => true,
})
}
}
/// Match a complete route against to a `SourceDeclSpec` by following `subdir`
/// designations along route and delegating to `Matches<CapabilityDecl>`
/// implementation.
impl Matches<Vec<RouteSegment>> for SourceDeclSpec {
const NAME: &'static str = "source declaration spec";
const OTHER_NAME: &'static str = "route";
fn matches(&self, other: &Vec<RouteSegment>) -> Result<bool> {
// Spec cannot match empty route.
let decl = other.last();
if decl.is_none() {
return Ok(false);
}
let decl = decl.unwrap();
// Accumulate `subdir` onto `decl.capability.source_path`, where applicable, and delegate
// to `self.matches(&CapabilityDecl)`.
match decl {
RouteSegment::DeclareBy { capability, .. } => match &capability {
CapabilityDecl::Directory(decl) => {
if self.path_prefix.is_none() || decl.source_path.is_none() {
return Ok(false);
}
let path_prefix = self.path_prefix.as_ref().unwrap();
let source_path = &decl.source_path.as_ref().unwrap();
let subdirs = get_subdirs(other);
let source_path_str = subdirs.iter().fold(source_path.to_path_buf(), |path_buf, next| {
let mut next_buf = path_buf.clone();
next_buf.push(next);
next_buf
}).to_str().ok_or_else(|| anyhow!("Failed to format PathBuf as string; components; {:?} appended with {:?}", decl.source_path, subdirs))?.to_string();
let source_path =
CapabilityPath::from_str(&source_path_str).with_context(|| {
anyhow!(
"Failed to parse string into CapabilityPath: {}",
source_path_str
)
})?;
Ok(match_path_prefix(path_prefix, &source_path))
}
_ => Ok(false),
},
_ => Ok(false),
}
}
}
fn get_subdirs(route: &Vec<RouteSegment>) -> Vec<PathBuf> {
let mut subdir = vec![];
for segment in route.iter() {
match segment {
RouteSegment::UseBy { capability, .. } => match capability {
UseDecl::Directory(decl) => {
if let Some(decl_subdir) = &decl.subdir {
subdir.push(decl_subdir.clone());
}
}
_ => {}
},
RouteSegment::OfferBy { capability, .. } => match capability {
OfferDecl::Directory(decl) => {
if let Some(decl_subdir) = &decl.subdir {
subdir.push(decl_subdir.clone());
}
}
_ => {}
},
RouteSegment::ExposeBy { capability, .. } => match capability {
ExposeDecl::Directory(decl) => {
if let Some(decl_subdir) = &decl.subdir {
subdir.push(decl_subdir.clone());
}
}
_ => {}
},
RouteSegment::DeclareBy { capability, .. } => match capability {
CapabilityDecl::Storage(decl) => {
if let Some(decl_subdir) = &decl.subdir {
subdir.push(decl_subdir.clone());
}
}
_ => {}
},
_ => {}
}
}
// Subdirs collected target->source, but are applied source->target.
subdir.reverse();
subdir
}
fn match_path_prefix(prefix: &CapabilityPath, path: &CapabilityPath) -> bool {
let prefix = prefix.split();
let path = path.split();
if prefix.len() > path.len() {
return false;
}
for (i, expected_segment) in prefix.iter().enumerate() {
let actual_segment = &path[i];
if expected_segment != actual_segment {
return false;
}
}
true
}
/// Output type: Wrapper for full set of results and dependencies.
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub struct VerifyRouteSourcesResults {
pub deps: HashSet<PathBuf>,
pub results: HashMap<String, Vec<VerifyRouteSourcesResult>>,
}
/// Output type: The result of matching a route use+source specification against
/// routes in the component tree.
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub struct VerifyRouteSourcesResult {
pub query: RouteMatch,
pub result: Result<Source, RouteSourceError>,
}
/// Output type: The source of a capability route.
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub struct Source {
/// The node path of the declaring component instance.
node_path: NodePath,
/// The capability declaration.
capability: CapabilityDecl,
}
/// Output type: Structured errors for matching a route+use specification
/// against routes in the component tree.
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub enum RouteSourceError {
CapabilityRouteError(CapabilityRouteError),
RouteSegmentWithoutComponent(RouteSegment),
RouteSegmentNodePathNotFoundInTree(RouteSegment),
ComponentInstanceLookupByUrlFailed(String),
MultipleComponentsWithSameUrl(Vec<Component>),
RouteSegmentComponentFromUntrustedSource(RouteSegment, ComponentSource),
RouteMismatch(Source),
MissingSourceCapability(RouteSegment),
InvalidUrl(String),
}
/// Intermediate value for use declarations that match a use+source spec.
struct Binding<'a> {
route_match: &'a RouteMatch,
use_decl: &'a UseDecl,
}
#[derive(Debug, ThisError)]
enum MatchOneError {
#[error("{0:?}")]
NoMatch(Error),
#[error("{0:?}")]
MultipleMatches(Error),
}
/// Trait for specifying a match between implementer and `Other` type values,
/// and matching exactly one `Other` from a vector. Matching functions may
/// return an error when input processing operations fail unexpectedly.
trait Matches<Other>: fmt::Debug + Sized + Serialize
where
Other: fmt::Debug,
{
const NAME: &'static str;
const OTHER_NAME: &'static str;
fn matches(&self, other: &Other) -> Result<bool>;
fn match_one<'a>(&self, other: &'a Vec<Other>) -> Result<&'a Other, MatchOneError> {
// Ignore match errors: Only interested in number of positive matches.
let matches: Vec<&'a Other> = other
.iter()
.filter_map(|other| match self.matches(other) {
Ok(true) => Some(other),
_ => None,
})
.collect();
if matches.len() == 0 {
return Err(MatchOneError::NoMatch(anyhow!(
"{}; no {}: {:#?} in {} list: {:#?}",
MATCH_ONE_FAILED,
Self::NAME,
self,
Self::OTHER_NAME,
other
)));
} else if matches.len() > 1 {
return Err(MatchOneError::MultipleMatches(anyhow!(
"Multiple instances of {} in {:#?} matches {} {:#?}; matches: {:#?}",
Self::OTHER_NAME,
other,
Self::NAME,
self,
matches
)));
}
Ok(matches[0])
}
}
/// Helper for attempting to embed JSON in error messages.
fn json_or_unformatted<S>(value: &S, type_name: &str) -> String
where
S: Serialize,
{
serde_json::to_string_pretty(value).unwrap_or_else(|_| format!("<unformatted {}>", type_name))
}
/// Attempt to gather exactly one match for all `routes_to_skip` and all
/// `routes_to_verify` in `component_routes`.
fn gather_routes<'a>(
component_routes: &'a RouteSourcesSpec,
component_decl: &'a ComponentDecl,
node_path: &'a NodePath,
) -> Result<(Vec<&'a UseDecl>, Vec<Binding<'a>>)> {
let uses = &component_decl.uses;
let routes_to_skip = component_routes
.routes_to_skip
.iter()
.filter_map(|use_spec| match use_spec.match_one(uses) {
// Allow no match on `routes_to_skip` to support soft transitions.
Err(MatchOneError::NoMatch(_)) => None,
result => Some(result.map_err(|err| err.into())),
})
.collect::<Result<Vec<&UseDecl>>>()?;
let routes_to_verify = component_routes
.routes_to_verify
.iter()
.map(|route_match| {
route_match
.target
.match_one(uses)
.map(|use_decl| Binding { route_match, use_decl })
.map_err(|err| err.into())
})
.collect::<Result<Vec<Binding<'a>>>>()?;
let mut all_matches = routes_to_skip.clone();
all_matches.extend(routes_to_verify.iter().map(|binding| binding.use_decl));
let mut deduped_matches: Vec<&UseDecl> = vec![];
let mut duped_matches: Vec<&UseDecl> = vec![];
for i in 0..all_matches.len() {
if duped_matches.contains(&all_matches[i]) {
continue;
}
for j in i + 1..all_matches.len() {
if all_matches[i] == all_matches[j] {
duped_matches.push(all_matches[i]);
continue;
}
}
deduped_matches.push(all_matches[i]);
}
if deduped_matches.len() < uses.len() {
let missed_routes: Vec<&UseDecl> =
uses.iter().filter(|use_decl| !deduped_matches.contains(use_decl)).collect();
if duped_matches.len() > 0 {
Err(anyhow!(
"{}. {}; component node path: {} routes matched multiple times: {:#?}; unmatched routes: {:#?}",
ROUTE_LISTS_OVERLAP,
ROUTE_LISTS_INCOMPLETE,
node_path,
duped_matches,
missed_routes
))
} else {
Err(anyhow!(
"{}; component node path: {}; unmatched routes: {:#?}",
ROUTE_LISTS_INCOMPLETE,
node_path,
missed_routes
))
}
} else if duped_matches.len() > 0 {
Err(anyhow!(
"{}; component node path: {}; routes matched multiple times: {:#?}",
ROUTE_LISTS_OVERLAP,
node_path,
duped_matches
))
} else {
Ok((routes_to_skip, routes_to_verify))
}
}
fn check_pkg_source(
route_segment: &RouteSegment,
component_model: &Arc<ComponentModelForAnalyzer>,
components: &Vec<Component>,
) -> Option<RouteSourceError> {
let node_path = route_segment.node_path();
if node_path.is_none() {
return Some(RouteSourceError::RouteSegmentWithoutComponent(route_segment.clone()));
}
let node_path = node_path.unwrap();
let get_instance_result = component_model.get_instance(node_path);
if get_instance_result.is_err() {
return Some(RouteSourceError::RouteSegmentNodePathNotFoundInTree(route_segment.clone()));
}
let instance = get_instance_result.unwrap();
let instance_url_str = instance.url();
let instance_url = match Url::parse(instance_url_str) {
Ok(url) => url,
Err(_) => return Some(RouteSourceError::InvalidUrl(instance_url_str.to_string())),
};
let matches: Vec<&Component> =
components.iter().filter(|component| &component.url == &instance_url).collect();
if matches.len() == 0 {
return Some(RouteSourceError::ComponentInstanceLookupByUrlFailed(
instance_url_str.to_string(),
));
}
if matches.len() > 1 {
return Some(RouteSourceError::MultipleComponentsWithSameUrl(
matches.iter().map(|&component| component.clone()).collect(),
));
}
match &matches[0].source {
ComponentSource::ZbiBootfs | ComponentSource::StaticPackage(_) => None,
source => Some(RouteSourceError::RouteSegmentComponentFromUntrustedSource(
route_segment.clone(),
source.clone(),
)),
}
}
fn process_verify_result<'a>(
verify_result: VerifyRouteResult,
route: &Binding<'a>,
component_model: &Arc<ComponentModelForAnalyzer>,
components: &Vec<Component>,
) -> Result<Result<Source, RouteSourceError>> {
match verify_result.result {
Ok(route_details) => {
for route_segment in route_details.iter() {
if let Some(err) = check_pkg_source(route_segment, component_model, components) {
return Ok(Err(err));
}
}
let route_source = route_details.last().ok_or_else(|| {
anyhow!(
"Route verifier traced empty route for capability matching {:?}",
json_or_unformatted(&route.route_match.target, "route target")
)
})?;
if let RouteSegment::DeclareBy { node_path, capability } = route_source {
let source =
Source { node_path: node_path.clone(), capability: capability.clone() };
let matches_result: Result<Vec<bool>> = vec![
route.route_match.source.capability.matches(capability),
route.route_match.source.capability.matches(&route_details),
]
.into_iter()
.map(|r| r)
.collect();
match matches_result {
Ok(source_and_cap_res) => {
if source_and_cap_res[0] && source_and_cap_res[1] {
Ok(Ok(source))
} else {
Ok(Err(RouteSourceError::RouteMismatch(source)))
}
}
Err(_) => Ok(Err(RouteSourceError::RouteMismatch(source))),
}
} else {
Ok(Err(RouteSourceError::MissingSourceCapability(route_source.clone())))
}
}
Err(err) => Ok(Err(RouteSourceError::CapabilityRouteError(err))),
}
}
/// `DataController` for verifying specific routes used by specific components.
#[derive(Default)]
pub struct RouteSourcesController {}
impl RouteSourcesController {
fn run(
&self,
component_model: &Arc<ComponentModelForAnalyzer>,
components: &Vec<Component>,
config: &RouteSourcesConfig,
) -> Result<HashMap<String, Vec<VerifyRouteSourcesResult>>> {
let mut results = HashMap::new();
for component_routes in config.component_routes.iter() {
let target_node_path = &component_routes.target_node_path;
let target_instance =
component_model.get_instance(target_node_path).context(format!(
"{}; target instance: {}",
MISSING_TARGET_INSTANCE,
target_node_path.clone()
))?;
let (_, routes_to_verify) = gather_routes(
component_routes,
target_instance.decl_for_testing(),
target_node_path,
)
.context(format!(
"{}; target instance: {}",
GATHER_FAILED,
target_node_path.clone()
))?;
let mut component_results = Vec::new();
for route in routes_to_verify.into_iter() {
// For some capabilities, a single use declaration can result in 2 route verifications
// (e.g. for storage capabilities, we check routing for both the storage capability itself
// and for its backing directory capability.)
for verify_result in
component_model.check_use_capability(route.use_decl, &target_instance)
{
let result =
process_verify_result(verify_result, &route, component_model, components)?;
let query = route.route_match.clone();
component_results.push(VerifyRouteSourcesResult { query, result });
}
}
results.insert(format!("/{}", target_node_path.as_vec().join("/")), component_results);
}
Ok(results)
}
}
impl DataController for RouteSourcesController {
fn query(&self, model: Arc<DataModel>, request: Value) -> Result<Value, Error> {
let request: RouteSourcesRequest =
serde_json::from_value(request).context(BAD_REQUEST_CTX)?;
let config_data = read_to_string(&request.input).map_err(|err| {
anyhow!("Failed to parse config from file: {}: {}", &request.input, err.to_string())
})?;
let config: RouteSourcesConfig = serde_json5::from_str(&config_data).map_err(|err| {
anyhow!("Failed to parse config from file: {}: {}", &request.input, err.to_string())
})?;
let component_model_result = model.get::<V2ComponentModel>()?;
let component_model = &component_model_result.component_model;
let components = &model.get::<Components>()?.entries;
let results = self.run(component_model, components, &config)?;
let deps = component_model_result.deps.clone();
Ok(json!(VerifyRouteSourcesResults { deps, results }))
}
}
#[cfg(test)]
mod tests {
use {
super::{
Matches, RouteMatch, RouteSourceError, RouteSourcesConfig, RouteSourcesController,
RouteSourcesSpec, Source, SourceDeclSpec, SourceSpec, UseSpec,
VerifyRouteSourcesResult, BAD_REQUEST_CTX, MISSING_TARGET_INSTANCE,
ROUTE_LISTS_INCOMPLETE, ROUTE_LISTS_OVERLAP,
},
crate::{
core::collection::{Component, ComponentSource, Components},
verify::{collection::V2ComponentModel, collector::component_model::DEFAULT_ROOT_URL},
},
anyhow::Result,
cm_fidl_analyzer::{
component_model::ModelBuilderForAnalyzer, node_path::NodePath, route::RouteSegment,
},
cm_rust::{
Availability, CapabilityName, CapabilityPath, CapabilityTypeName, ChildDecl,
ComponentDecl, DependencyType, DirectoryDecl, ExposeDirectoryDecl, ExposeSource,
ExposeTarget, OfferDirectoryDecl, OfferSource, OfferTarget, ProgramDecl,
UseDirectoryDecl, UseSource, UseStorageDecl,
},
fidl_fuchsia_component_decl as fdecl, fidl_fuchsia_io as fio,
fuchsia_merkle::{Hash, HASH_SIZE},
maplit::{hashmap, hashset},
routing::{
component_id_index::ComponentIdIndex, config::RuntimeConfig,
environment::RunnerRegistry,
},
scrutiny::prelude::{DataController, DataModel},
scrutiny_testing::fake::fake_data_model,
serde_json::json,
std::{path::PathBuf, str::FromStr, sync::Arc},
url::Url,
};
const TEST_URL_PREFIX: &str = "fuchsia-pkg://test.fuchsia.com";
fn make_test_url(component_name: &str) -> Url {
Url::parse(&format!("{}/{}#meta/{}.cm", TEST_URL_PREFIX, component_name, component_name))
.expect("test URL to parse")
}
#[fuchsia::test]
fn test_match_some_only() {
// Match Some(path), ignoring None name.
assert!(
UseSpec {
type_name: CapabilityTypeName::Storage,
path: Some(CapabilityPath::from_str("/path").unwrap()),
name: None,
}
.matches(
&UseStorageDecl {
source_name: CapabilityName("name".to_string()),
target_path: CapabilityPath::from_str("/path").unwrap(),
availability: Availability::Required,
}
.into()
)
.unwrap()
== true
);
// Match Some(name), ignoring None path.
assert!(
UseSpec {
type_name: CapabilityTypeName::Storage,
path: None,
name: Some(CapabilityName("name".to_string())),
}
.matches(
&UseStorageDecl {
source_name: CapabilityName("name".to_string()),
target_path: CapabilityPath::from_str("/path").unwrap(),
availability: Availability::Required,
}
.into()
)
.unwrap()
== true
);
}
macro_rules! ok_unwrap {
($actual_result:expr) => {{
let actual_result = $actual_result;
let actual_ref = actual_result.as_ref();
if actual_ref.is_err() {
println!("Unexpected Err");
for err in actual_ref.err().unwrap().chain() {
println!(" {}", err.to_string());
}
}
actual_result.ok().unwrap()
}};
}
macro_rules! err_unwrap {
($actual_result:expr) => {{
let actual_result = $actual_result;
let actual_ref = actual_result.as_ref();
if actual_ref.is_ok() {
println!("Unexpected Ok");
println!(" {:#?}", actual_ref.ok().unwrap());
}
actual_result.err().unwrap()
}};
}
macro_rules! err_starts_with {
($err:expr, $prefix:expr) => {{
if !$err.root_cause().to_string().starts_with($prefix) {
println!("Error root cause does not start with \"{}\"", $prefix);
for e in $err.chain() {
println!(" {}", e.to_string());
}
}
assert!($err.root_cause().to_string().starts_with($prefix));
}};
}
macro_rules! err_contains {
($err:expr, $substr:expr) => {{
if !$err.root_cause().to_string().contains($substr) {
println!("Error root cause does not contain \"{}\"", $substr);
for e in $err.chain() {
println!(" {}", e.to_string());
}
}
assert!($err.root_cause().to_string().contains($substr));
}};
}
fn create_component(url: &Url, source: ComponentSource) -> Component {
Component { id: 0, url: url.clone(), version: 0, source }
}
// Component tree:
//
// ________root_url________
// / \
// two_dir_user_url one_dir_provider_url
// + root_url child name: + root_url child name:
// two_dir_user one_dir_provider
//
// Directory routes:
//
// - Component URLs prefixed by @
// - Capability prefixed by $
// - Directories and subdirectories inside ()
// - Subdirectories have no leading /
//
// @one_dir_provider_url: $provider_dir(/data/to/user)
// -- $exposed_by_provider(provider_subdir) -->
// @root_url
// -- $routed_from_provider(root_subdir) -->
// @two_dir_user_url
// -- (user_subdir) --> (/data/from/provider)
//
// I.e.,
// @one_dir_provider_url(/data/to/user/provider_subdir/root_subdir/user_subdir)
// binds to
// @two_dir_user_url(/data/from/provider)
//
// @root_url: $root_dir(/data/to/user)
// -- $routed_from_root(root_subdir) -->
// @two_dir_user_url
// -- (user_subdir) --> (/data/from/root)
//
// I.e.,
// @root_url(/data/to/user/root_subdir/user_subdir)
// binds to
// @two_dir_user_url(/data/from/root)
fn valid_two_instance_two_dir_tree_model(
data_model: Option<Arc<DataModel>>,
) -> Result<Arc<DataModel>> {
let root_url = &*DEFAULT_ROOT_URL;
let two_dir_user_url = make_test_url("two_dir_user");
let one_dir_provider_url = make_test_url("one_dir_provider");
let data_model = data_model.unwrap_or(fake_data_model());
let components = hashmap! {
root_url.clone() => ComponentDecl{
program: Some(ProgramDecl{ runner: Some("some_runner".into()), ..ProgramDecl::default()}),
capabilities: vec![
DirectoryDecl{
name: CapabilityName("root_dir".to_string()),
source_path: Some(CapabilityPath::from_str("/data/to/user").unwrap()),
rights: fio::Operations::CONNECT,
}.into(),
],
offers: vec![
OfferDirectoryDecl{
source: OfferSource::static_child("one_dir_provider".to_string()),
source_name: CapabilityName("exposed_by_provider".to_string()),
target: OfferTarget::static_child("two_dir_user".to_string()),
target_name: CapabilityName("routed_from_provider".to_string()),
dependency_type: DependencyType::Strong,
rights: Some(fio::Operations::CONNECT),
subdir: Some(PathBuf::from_str("root_subdir").unwrap()),
availability: Availability::Required,
}.into(),
OfferDirectoryDecl{
source: OfferSource::Self_,
source_name: CapabilityName("root_dir".to_string()),
target: OfferTarget::static_child("two_dir_user".to_string()),
target_name: CapabilityName("routed_from_root".to_string()),
dependency_type: DependencyType::Strong,
rights: Some(fio::Operations::CONNECT),
subdir: Some(PathBuf::from_str("root_subdir").unwrap()),
availability: Availability::Required,
}.into(),
],
children: vec![
ChildDecl{
name: "two_dir_user".to_string(),
url: two_dir_user_url.to_string(),
startup: fdecl::StartupMode::Lazy,
on_terminate: None,
environment: None,
},
ChildDecl{
name: "one_dir_provider".to_string(),
url: one_dir_provider_url.to_string(),
startup: fdecl::StartupMode::Lazy,
on_terminate: None,
environment: None,
},
],
..ComponentDecl::default()
},
two_dir_user_url => ComponentDecl{
uses: vec![
UseDirectoryDecl{
source: UseSource::Parent,
source_name: CapabilityName("routed_from_provider".to_string()),
target_path: CapabilityPath::from_str("/data/from/provider").unwrap(),
rights: fio::Operations::CONNECT,
subdir: Some(PathBuf::from_str("user_subdir").unwrap()),
dependency_type: DependencyType::Strong,
availability: Availability::Required,
}.into(),
UseDirectoryDecl{
source: UseSource::Parent,
source_name: CapabilityName("routed_from_root".to_string()),
target_path: CapabilityPath::from_str("/data/from/root").unwrap(),
rights: fio::Operations::CONNECT,
subdir: Some(PathBuf::from_str("user_subdir").unwrap()),
dependency_type: DependencyType::Strong,
availability: Availability::Required,
}.into(),
],
..ComponentDecl::default()
},
one_dir_provider_url => ComponentDecl{
program: Some(ProgramDecl{ runner: Some("some_runner".into()), ..ProgramDecl::default()}),
capabilities: vec![
DirectoryDecl{
name: CapabilityName("provider_dir".to_string()),
source_path: Some(CapabilityPath::from_str("/data/to/user").unwrap()),
rights: fio::Operations::CONNECT,
}.into(),
],
exposes: vec![
ExposeDirectoryDecl{
source: ExposeSource::Self_,
source_name: CapabilityName("provider_dir".to_string()),
target: ExposeTarget::Parent,
target_name: CapabilityName("exposed_by_provider".to_string()),
rights: Some(fio::Operations::CONNECT),
subdir: Some(PathBuf::from_str("provider_subdir").unwrap()),
}.into(),
],
..ComponentDecl::default()
},
};
let build_component_model = ModelBuilderForAnalyzer::new(root_url.clone()).build(
components,
Arc::new(RuntimeConfig::default()),
Arc::new(ComponentIdIndex::default()),
RunnerRegistry::default(),
);
let deps = hashset! {};
data_model.set(V2ComponentModel::new(
deps,
build_component_model.model.expect("component model to build"),
build_component_model.errors,
))?;
Ok(data_model)
}
fn valid_two_instance_two_dir_components_model(
data_model: Option<Arc<DataModel>>,
) -> Result<Arc<DataModel>> {
let data_model = data_model.unwrap_or(fake_data_model());
let components = vec![
create_component(&*DEFAULT_ROOT_URL, ComponentSource::ZbiBootfs),
create_component(
&make_test_url("two_dir_user"),
ComponentSource::StaticPackage(Hash::from([0u8; HASH_SIZE])),
),
create_component(
&make_test_url("one_dir_provider"),
ComponentSource::StaticPackage(Hash::from([1u8; HASH_SIZE])),
),
];
data_model.set(Components { entries: components })?;
Ok(data_model)
}
fn two_instance_two_dir_components_model_missing_user(
data_model: Option<Arc<DataModel>>,
) -> Result<Arc<DataModel>> {
let data_model = data_model.unwrap_or(fake_data_model());
let components = vec![
create_component(&*DEFAULT_ROOT_URL, ComponentSource::ZbiBootfs),
create_component(
&make_test_url("one_dir_provider"),
ComponentSource::StaticPackage(Hash::from([0u8; HASH_SIZE])),
),
];
data_model.set(Components { entries: components })?;
Ok(data_model)
}
fn two_instance_two_dir_components_model_duplicate_user(
data_model: Option<Arc<DataModel>>,
) -> Result<(Arc<DataModel>, Vec<Component>)> {
let two_dir_user_url = make_test_url("two_dir_user");
let one_dir_provider_url = make_test_url("one_dir_provider");
let data_model = data_model.unwrap_or(fake_data_model());
let components = vec![
create_component(&*DEFAULT_ROOT_URL, ComponentSource::ZbiBootfs),
create_component(
&two_dir_user_url,
ComponentSource::StaticPackage(Hash::from([0u8; HASH_SIZE])),
),
create_component(
&two_dir_user_url,
ComponentSource::StaticPackage(Hash::from([1u8; HASH_SIZE])),
),
create_component(
&one_dir_provider_url,
ComponentSource::StaticPackage(Hash::from([3u8; HASH_SIZE])),
),
];
data_model.set(Components { entries: components })?;
Ok((
data_model,
vec![
create_component(
&two_dir_user_url,
ComponentSource::StaticPackage(Hash::from([0u8; HASH_SIZE])),
),
create_component(
&two_dir_user_url,
ComponentSource::StaticPackage(Hash::from([1u8; HASH_SIZE])),
),
],
))
}
fn two_instance_two_dir_components_model_untrusted_user_source(
data_model: Option<Arc<DataModel>>,
) -> Result<(Arc<DataModel>, ComponentSource)> {
let data_model = data_model.unwrap_or(fake_data_model());
let untrusted_source = ComponentSource::Package(Hash::from([0u8; HASH_SIZE]));
let components = vec![
create_component(&*DEFAULT_ROOT_URL, ComponentSource::ZbiBootfs),
create_component(&make_test_url("two_dir_user"), untrusted_source.clone()),
create_component(
&make_test_url("one_dir_provider"),
ComponentSource::StaticPackage(Hash::from([1u8; HASH_SIZE])),
),
];
data_model.set(Components { entries: components })?;
Ok((data_model, untrusted_source))
}
#[fuchsia::test]
fn test_component_routes_bad_request() -> Result<()> {
let data_model = valid_two_instance_two_dir_tree_model(Some(
valid_two_instance_two_dir_components_model(None)?,
))?;
let controller = RouteSourcesController::default();
assert_eq!(
// Request JSON is invalid.
controller.query(data_model, json!({"invalid": "request"})).err().unwrap().to_string(),
BAD_REQUEST_CTX
);
Ok(())
}
#[fuchsia::test]
fn test_component_routes_target_ok() -> Result<()> {
let data_model = valid_two_instance_two_dir_tree_model(Some(
valid_two_instance_two_dir_components_model(None)?,
))?;
let controller = RouteSourcesController::default();
let component_model = &data_model.get::<V2ComponentModel>()?.component_model;
let components = &data_model.get::<Components>()?.entries;
// Vacuous request: Confirms that @root_url uses no input capabilities.
let config = RouteSourcesConfig {
component_routes: vec![RouteSourcesSpec {
target_node_path: NodePath::absolute_from_vec(vec![]),
routes_to_skip: vec![],
routes_to_verify: vec![],
}],
};
ok_unwrap!(controller.run(component_model, components, &config));
Ok(())
}
#[fuchsia::test]
fn test_component_routes_missing_target() -> Result<()> {
let data_model = valid_two_instance_two_dir_tree_model(Some(
valid_two_instance_two_dir_components_model(None)?,
))?;
let controller = RouteSourcesController::default();
let component_model = &data_model.get::<V2ComponentModel>()?.component_model;
let components = &data_model.get::<Components>()?.entries;
// Request checking routes of a component instance that does not exist.
let config = RouteSourcesConfig {
component_routes: vec![RouteSourcesSpec {
target_node_path: NodePath::absolute_from_vec(vec!["does:0", "not:1", "exist:2"]),
routes_to_skip: vec![],
routes_to_verify: vec![],
}],
};
let err = err_unwrap!(controller.run(component_model, components, &config));
// Not using `err_start_with` because matching `err` string, not
// `err.root_cause()` string; `MISSING_TARGET_INSTANCE` is the last context
// attached to the error, which originates elsewhere.
assert!(err.to_string().starts_with(MISSING_TARGET_INSTANCE));
Ok(())
}
#[fuchsia::test]
fn test_route_lists_incomplete() -> Result<()> {
let data_model = valid_two_instance_two_dir_tree_model(Some(
valid_two_instance_two_dir_components_model(None)?,
))?;
let controller = RouteSourcesController::default();
let component_model = &data_model.get::<V2ComponentModel>()?.component_model;
let components = &data_model.get::<Components>()?.entries;
// Request fails because not all capabilities used by @two_dir_user_url
// are listed in `routes_to_skip` + `routes_to_verify`.
let config = RouteSourcesConfig {
component_routes: vec![RouteSourcesSpec {
target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
routes_to_skip: vec![],
routes_to_verify: vec![],
}],
};
let err = err_unwrap!(controller.run(component_model, components, &config,));
assert!(err.root_cause().to_string().starts_with(ROUTE_LISTS_INCOMPLETE));
Ok(())
}
#[fuchsia::test]
fn test_skip_all_routes() -> Result<()> {
let data_model = valid_two_instance_two_dir_tree_model(Some(
valid_two_instance_two_dir_components_model(None)?,
))?;
let controller = RouteSourcesController::default();
let component_model = &data_model.get::<V2ComponentModel>()?.component_model;
let components = &data_model.get::<Components>()?.entries;
// Successful request that confirms but does not verify sources on all capabilities used by
// @two_dir_user_url.
let config = RouteSourcesConfig {
component_routes: vec![RouteSourcesSpec {
target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
routes_to_skip: vec![
UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/provider").unwrap()),
name: None,
},
UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/root").unwrap()),
name: None,
},
],
routes_to_verify: vec![],
}],
};
let result = ok_unwrap!(controller.run(component_model, components, &config));
assert_eq!(
result,
hashmap! {
"/two_dir_user".to_string() => vec![] as Vec<VerifyRouteSourcesResult>,
},
);
Ok(())
}
#[fuchsia::test]
fn test_skip_extra_route() -> Result<()> {
let data_model = valid_two_instance_two_dir_tree_model(Some(
valid_two_instance_two_dir_components_model(None)?,
))?;
let controller = RouteSourcesController::default();
let component_model = &data_model.get::<V2ComponentModel>()?.component_model;
let components = &data_model.get::<Components>()?.entries;
// Successful request that confirms but does not verify sources on all
// capabilities used by @two_dir_user_url, and lists an extra route to
// skip. This pattern is permitted to allow for soft transitions where
// new routes are added to `routes_to_skip` config first, then added to
// the corresponding component manifest.
let config = RouteSourcesConfig {
component_routes: vec![RouteSourcesSpec {
target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
routes_to_skip: vec![
UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/provider").unwrap()),
name: None,
},
UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/root").unwrap()),
name: None,
},
UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/new/dir/route").unwrap()),
name: None,
},
],
routes_to_verify: vec![],
}],
};
let result = ok_unwrap!(controller.run(component_model, components, &config));
assert_eq!(
result,
hashmap! {
"/two_dir_user".to_string() => vec![] as Vec<VerifyRouteSourcesResult>,
},
);
Ok(())
}
#[fuchsia::test]
fn test_match_some_routes() -> Result<()> {
let data_model = valid_two_instance_two_dir_tree_model(Some(
valid_two_instance_two_dir_components_model(None)?,
))?;
let controller = RouteSourcesController::default();
let component_model = &data_model.get::<V2ComponentModel>()?.component_model;
let components = &data_model.get::<Components>()?.entries;
let config = RouteSourcesConfig {
component_routes: vec![RouteSourcesSpec {
target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
routes_to_skip: vec![
// Skip @root_url -> @two_dir_user_url route.
UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/root").unwrap()),
name: None,
},
],
routes_to_verify: vec![
// config.component_routes[0].routes_to_verify[0]:
// Match route:
// @one_dir_provider_url
// -> @root_url
// -> @one_dir_user_url.
RouteMatch {
target: UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/provider").unwrap()),
name: None,
},
source: SourceSpec {
node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
capability: SourceDeclSpec {
// Match complete path with routed subdirs.
path_prefix: Some(
CapabilityPath::from_str(
"/data/to/user/provider_subdir/root_subdir/user_subdir",
)
.unwrap(),
),
name: Some(CapabilityName("provider_dir".to_string())),
},
},
},
],
}],
};
let result = ok_unwrap!(controller.run(component_model, components, &config));
assert_eq!(
result,
hashmap! {
"/two_dir_user".to_string() => vec![
VerifyRouteSourcesResult{
query: config.component_routes[0].routes_to_verify[0].clone(),
result: Ok(Source {
node_path: config.component_routes[0].routes_to_verify[0].source.node_path.clone(),
capability: DirectoryDecl{
name: CapabilityName("provider_dir".to_string()),
source_path: Some(CapabilityPath::from_str("/data/to/user").unwrap()),
rights: fio::Operations::CONNECT,
}.into(),
})
}
],
}
);
Ok(())
}
#[fuchsia::test]
fn test_match_some_routes_partial_path() -> Result<()> {
let data_model = valid_two_instance_two_dir_tree_model(Some(
valid_two_instance_two_dir_components_model(None)?,
))?;
let controller = RouteSourcesController::default();
let component_model = &data_model.get::<V2ComponentModel>()?.component_model;
let components = &data_model.get::<Components>()?.entries;
let config = RouteSourcesConfig {
component_routes: vec![RouteSourcesSpec {
target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
routes_to_skip: vec![
// Skip @root_url -> @two_dir_user_url route.
UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/root").unwrap()),
name: None,
},
],
routes_to_verify: vec![
// config.component_routes[0].routes_to_verify[0]:
// Match route:
// @one_dir_provider_url
// -> @root_url
// -> @one_dir_user_url.
RouteMatch {
target: UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/provider").unwrap()),
name: None,
},
source: SourceSpec {
node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
capability: SourceDeclSpec {
// Match partial path with some (not all) routed
// subdirs.
path_prefix: Some(
CapabilityPath::from_str(
"/data/to/user/provider_subdir/root_subdir",
)
.unwrap(),
),
name: Some(CapabilityName("provider_dir".to_string())),
},
},
},
],
}],
};
let result = ok_unwrap!(controller.run(component_model, components, &config));
assert_eq!(
result,
hashmap! {
"/two_dir_user".to_string() => vec![
VerifyRouteSourcesResult{
query: config.component_routes[0].routes_to_verify[0].clone(),
result: Ok(Source {
node_path: config.component_routes[0].routes_to_verify[0].source.node_path.clone(),
capability: DirectoryDecl{
name: CapabilityName("provider_dir".to_string()),
source_path: Some(CapabilityPath::from_str("/data/to/user").unwrap()),
rights: fio::Operations::CONNECT,
}.into(),
})
}
],
}
);
Ok(())
}
#[fuchsia::test]
fn test_match_all_routes() -> Result<()> {
let data_model = valid_two_instance_two_dir_tree_model(Some(
valid_two_instance_two_dir_components_model(None)?,
))?;
let controller = RouteSourcesController::default();
let component_model = &data_model.get::<V2ComponentModel>()?.component_model;
let components = &data_model.get::<Components>()?.entries;
let config = RouteSourcesConfig {
component_routes: vec![RouteSourcesSpec {
target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
routes_to_skip: vec![],
routes_to_verify: vec![
// config.component_routes[0].routes_to_verify[0]:
// Match route: @root_url -> @two_dir_user_url route.
RouteMatch {
target: UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/root").unwrap()),
name: None,
},
source: SourceSpec {
node_path: NodePath::absolute_from_vec(vec![]),
capability: SourceDeclSpec {
// Match complete path with routed subdirs.
path_prefix: Some(
CapabilityPath::from_str(
"/data/to/user/root_subdir/user_subdir",
)
.unwrap(),
),
name: Some(CapabilityName("root_dir".to_string())),
},
},
},
// config.component_routes[0].routes_to_verify[1]:
// Match route:
// @one_dir_provider_url
// -> @root_url
// -> @one_dir_user_url.
RouteMatch {
target: UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/provider").unwrap()),
name: None,
},
source: SourceSpec {
node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
capability: SourceDeclSpec {
// Match complete path with routed subdirs.
path_prefix: Some(
CapabilityPath::from_str(
"/data/to/user/provider_subdir/root_subdir/user_subdir",
)
.unwrap(),
),
name: Some(CapabilityName("provider_dir".to_string())),
},
},
},
],
}],
};
let result = ok_unwrap!(controller.run(component_model, components, &config));
assert_eq!(
result,
hashmap! {
"/two_dir_user".to_string() => vec![
VerifyRouteSourcesResult{
query: config.component_routes[0].routes_to_verify[0].clone(),
result: Ok(Source {
node_path: config.component_routes[0].routes_to_verify[0].source.node_path.clone(),
capability: DirectoryDecl{
name: CapabilityName("root_dir".to_string()),
source_path: Some(CapabilityPath::from_str("/data/to/user").unwrap()),
rights: fio::Operations::CONNECT,
}.into(),
})
},
VerifyRouteSourcesResult{
query: config.component_routes[0].routes_to_verify[1].clone(),
result: Ok(Source {
node_path: config.component_routes[0].routes_to_verify[1].source.node_path.clone(),
capability: DirectoryDecl{
name: CapabilityName("provider_dir".to_string()),
source_path: Some(CapabilityPath::from_str("/data/to/user").unwrap()),
rights: fio::Operations::CONNECT,
}.into(),
})
}
],
}
);
Ok(())
}
#[fuchsia::test]
fn test_match_multiple_components() -> Result<()> {
let data_model = valid_two_instance_two_dir_tree_model(Some(
valid_two_instance_two_dir_components_model(None)?,
))?;
let controller = RouteSourcesController::default();
let component_model = &data_model.get::<V2ComponentModel>()?.component_model;
let components = &data_model.get::<Components>()?.entries;
let config = RouteSourcesConfig {
component_routes: vec![
// Match empty set of routes used by @root_url.
RouteSourcesSpec {
target_node_path: NodePath::absolute_from_vec(vec![]),
routes_to_skip: vec![],
routes_to_verify: vec![],
},
// Match all routes used by @two_dir_user_url.
RouteSourcesSpec {
target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
routes_to_skip: vec![],
routes_to_verify: vec![
// config.component_routes[1].routes_to_verify[0]:
// Match route: @root_url -> @two_dir_user_url route.
RouteMatch {
target: UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/root").unwrap()),
name: None,
},
source: SourceSpec {
node_path: NodePath::absolute_from_vec(vec![]),
capability: SourceDeclSpec {
path_prefix: Some(
CapabilityPath::from_str(
"/data/to/user/root_subdir/user_subdir",
)
.unwrap(),
),
name: Some(CapabilityName("root_dir".to_string())),
},
},
},
// config.component_routes[1].routes_to_verify[1]:
// Match route:
// @one_dir_provider_url
// -> @root_url
// -> @one_dir_user_url.
RouteMatch {
target: UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(
CapabilityPath::from_str("/data/from/provider").unwrap(),
),
name: None,
},
source: SourceSpec {
node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
capability: SourceDeclSpec {
path_prefix: Some(
CapabilityPath::from_str(
"/data/to/user/provider_subdir/root_subdir/user_subdir",
)
.unwrap(),
),
name: Some(CapabilityName("provider_dir".to_string())),
},
},
},
],
},
],
};
let result = ok_unwrap!(controller.run(component_model, components, &config));
assert_eq!(
result,
hashmap! {
"/".to_string() => vec![] as Vec<VerifyRouteSourcesResult>,
"/two_dir_user".to_string() => vec![
VerifyRouteSourcesResult{
query: config.component_routes[1].routes_to_verify[0].clone(),
result: Ok(Source {
node_path: config.component_routes[1].routes_to_verify[0].source.node_path.clone(),
capability: DirectoryDecl{
name: CapabilityName("root_dir".to_string()),
source_path: Some(CapabilityPath::from_str("/data/to/user").unwrap()),
rights: fio::Operations::CONNECT,
}.into(),
})
},
VerifyRouteSourcesResult{
query: config.component_routes[1].routes_to_verify[1].clone(),
result: Ok(Source {
node_path: config.component_routes[1].routes_to_verify[1].source.node_path.clone(),
capability: DirectoryDecl{
name: CapabilityName("provider_dir".to_string()),
source_path: Some(CapabilityPath::from_str("/data/to/user").unwrap()),
rights: fio::Operations::CONNECT,
}.into(),
})
}
],
}
);
Ok(())
}
#[fuchsia::test]
fn test_misconfigured_skip_match_source_name() -> Result<()> {
let data_model = valid_two_instance_two_dir_tree_model(Some(
valid_two_instance_two_dir_components_model(None)?,
))?;
let component_model = &data_model.get::<V2ComponentModel>()?.component_model;
let components = &data_model.get::<Components>()?.entries;
let source_name = "routed_from_provider";
let config = RouteSourcesConfig {
component_routes: vec![RouteSourcesSpec {
target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
routes_to_skip: vec![
UseSpec {
type_name: CapabilityTypeName::Directory,
path: None,
// This is the source name of the route, but
// directory uses are matched by target path.
name: Some(CapabilityName(source_name.to_string())),
},
UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/root").unwrap()),
name: None,
},
],
routes_to_verify: vec![],
}],
};
let controller = RouteSourcesController::default();
let err = err_unwrap!(controller.run(component_model, components, &config));
// List appears incomplete because `routes_to_skip[0]` fails to match,
// and `routes_to_skip` is allowed to contain unmatched items to support
// soft transitions.
err_starts_with!(err, ROUTE_LISTS_INCOMPLETE);
// Correct target path should appear in error message; it is the
// unmatched route (unmatched because source name should not have been
// specified). `CapabilityPath` stores dirname and basename separately;
// they will appear separately in error string.
err_contains!(err, "/data/from");
err_contains!(err, "provider");
Ok(())
}
#[fuchsia::test]
fn test_misconfigured_skip_match_source_name_and_target_path() -> Result<()> {
let data_model = valid_two_instance_two_dir_tree_model(Some(
valid_two_instance_two_dir_components_model(None)?,
))?;
let component_model = &data_model.get::<V2ComponentModel>()?.component_model;
let components = &data_model.get::<Components>()?.entries;
// `CapabilityPath` stores dirname and basename separately; they will
// appear separately in error string.
let target_dirname = "/data/from";
let target_basename = "provider";
let target_path = format!("{}/{}", target_dirname, target_basename);
let source_name = "routed_from_provider";
let config = RouteSourcesConfig {
component_routes: vec![RouteSourcesSpec {
target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
routes_to_skip: vec![
UseSpec {
type_name: CapabilityTypeName::Directory,
// This is the correct path, but also specifying
// source name should cause a failure.
path: Some(CapabilityPath::from_str(&target_path).unwrap()),
// This is the source name of the route, but
// directory uses are matched by target path.
name: Some(CapabilityName(source_name.to_string())),
},
UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/root").unwrap()),
name: None,
},
],
routes_to_verify: vec![],
}],
};
let controller = RouteSourcesController::default();
let err = err_unwrap!(controller.run(component_model, components, &config));
// List appears incomplete because `routes_to_skip[0]` fails to match,
// and `routes_to_skip` is allowed to contain unmatched items to support
// soft transitions.
err_starts_with!(err, ROUTE_LISTS_INCOMPLETE);
// Correct target path should appear in error message; it is the
// unmatched route (unmatched because source name should not have been
// specified).
err_contains!(err, target_dirname);
err_contains!(err, target_basename);
err_contains!(err, source_name);
Ok(())
}
#[fuchsia::test]
fn test_skip_route_with_no_match() -> Result<()> {
let data_model = valid_two_instance_two_dir_tree_model(Some(
valid_two_instance_two_dir_components_model(None)?,
))?;
let component_model = &data_model.get::<V2ComponentModel>()?.component_model;
let components = &data_model.get::<Components>()?.entries;
// `CapabilityPath` stores dirname and basename separately; they will
// appear separately in error string.
let bad_dirname = "/does/not";
let bad_basename = "exist";
let bad_path = format!("{}/{}", bad_dirname, bad_basename);
let config = RouteSourcesConfig {
component_routes: vec![RouteSourcesSpec {
target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
routes_to_skip: vec![
UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/provider").unwrap()),
name: None,
},
UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/root").unwrap()),
name: None,
},
// Unmatched `routes_to_skip` entry will be skipped; this
// pattern is allowed so that soft transitions that update
// verification config first and component manifests second
// work as intended.
UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str(&bad_path).unwrap()),
name: None,
},
],
routes_to_verify: vec![],
}],
};
let controller = RouteSourcesController::default();
let result = ok_unwrap!(controller.run(component_model, components, &config));
assert_eq!(result, hashmap! {"/two_dir_user".to_string() => vec![]});
Ok(())
}
#[fuchsia::test]
fn test_skip_duplicate() -> Result<()> {
let data_model = valid_two_instance_two_dir_tree_model(Some(
valid_two_instance_two_dir_components_model(None)?,
))?;
let component_model = &data_model.get::<V2ComponentModel>()?.component_model;
let components = &data_model.get::<Components>()?.entries;
let dup_name = "/data/from/root";
let config = RouteSourcesConfig {
component_routes: vec![RouteSourcesSpec {
target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
routes_to_skip: vec![
UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/provider").unwrap()),
name: None,
},
UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str(dup_name).unwrap()),
name: None,
},
// Intentional error: Duplicate match for same
// route-to-skip.
UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str(dup_name).unwrap()),
name: None,
},
],
routes_to_verify: vec![],
}],
};
let controller = RouteSourcesController::default();
let err = err_unwrap!(controller.run(component_model, components, &config));
err_starts_with!(err, ROUTE_LISTS_OVERLAP);
Ok(())
}
#[fuchsia::test]
fn test_match_duplicate() -> Result<()> {
let data_model = valid_two_instance_two_dir_tree_model(Some(
valid_two_instance_two_dir_components_model(None)?,
))?;
let controller = RouteSourcesController::default();
let component_model = &data_model.get::<V2ComponentModel>()?.component_model;
let components = &data_model.get::<Components>()?.entries;
let config = RouteSourcesConfig {
component_routes: vec![RouteSourcesSpec {
target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
routes_to_skip: vec![UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/root").unwrap()),
name: None,
}],
routes_to_verify: vec![
RouteMatch {
target: UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/provider").unwrap()),
name: None,
},
source: SourceSpec {
node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
capability: SourceDeclSpec {
path_prefix: Some(
CapabilityPath::from_str(
"/data/to/user/provider_subdir/root_subdir/user_subdir",
)
.unwrap(),
),
name: Some(CapabilityName("provider_dir".to_string())),
},
},
},
// Intentional error: Duplicate match for same
// route-to-verify.
RouteMatch {
target: UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/root").unwrap()),
name: None,
},
source: SourceSpec {
node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
capability: SourceDeclSpec {
path_prefix: Some(
CapabilityPath::from_str("/data/to/user").unwrap(),
),
name: Some(CapabilityName("provider_dir".to_string())),
},
},
},
],
}],
};
let err = err_unwrap!(controller.run(component_model, components, &config));
err_starts_with!(err, ROUTE_LISTS_OVERLAP);
Ok(())
}
#[fuchsia::test]
fn test_skip_match_duplicate() -> Result<()> {
let data_model = valid_two_instance_two_dir_tree_model(Some(
valid_two_instance_two_dir_components_model(None)?,
))?;
let dup_name = "/data/from/provider";
let controller = RouteSourcesController::default();
let component_model = &data_model.get::<V2ComponentModel>()?.component_model;
let components = &data_model.get::<Components>()?.entries;
let config = RouteSourcesConfig {
component_routes: vec![RouteSourcesSpec {
target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
routes_to_skip: vec![
UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str(dup_name).unwrap()),
name: None,
},
UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/root").unwrap()),
name: None,
},
],
routes_to_verify: vec![
// Intentional error: Route-to-verify match is duplicate of
// a route-to-skip match.
RouteMatch {
target: UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str(dup_name).unwrap()),
name: None,
},
source: SourceSpec {
node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
capability: SourceDeclSpec {
path_prefix: Some(
CapabilityPath::from_str(
"/data/to/user/provider_subdir/root_subdir/user_subdir",
)
.unwrap(),
),
name: Some(CapabilityName("provider_dir".to_string())),
},
},
},
],
}],
};
let err = err_unwrap!(controller.run(component_model, components, &config));
err_starts_with!(err, ROUTE_LISTS_OVERLAP);
Ok(())
}
#[fuchsia::test]
fn test_skip_match_duplicate_mixed() -> Result<()> {
let data_model = valid_two_instance_two_dir_tree_model(Some(
valid_two_instance_two_dir_components_model(None)?,
))?;
let dup_name = "/data/from/provider";
let controller = RouteSourcesController::default();
let component_model = &data_model.get::<V2ComponentModel>()?.component_model;
let components = &data_model.get::<Components>()?.entries;
let config = RouteSourcesConfig {
component_routes: vec![RouteSourcesSpec {
target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
routes_to_skip: vec![
// Intentional error: No match for `/data/from/root`. That
// way number of routes to skip + number of routes to verify
// checks out, but route not every route is matched.
UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str(dup_name).unwrap()),
name: None,
},
],
routes_to_verify: vec![
// Intentional error: Route-to-verify match is duplicate of
// a route-to-skip match.
RouteMatch {
target: UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str(dup_name).unwrap()),
name: None,
},
source: SourceSpec {
node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
capability: SourceDeclSpec {
path_prefix: Some(
CapabilityPath::from_str(
"/data/to/user/provider_subdir/root_subdir/user_subdir",
)
.unwrap(),
),
name: Some(CapabilityName("provider_dir".to_string())),
},
},
},
],
}],
};
let err = err_unwrap!(controller.run(component_model, components, &config));
err_contains!(err, ROUTE_LISTS_OVERLAP);
err_contains!(err, ROUTE_LISTS_INCOMPLETE);
Ok(())
}
#[fuchsia::test]
fn test_verify_all_missing_user_component() -> Result<()> {
let data_model = valid_two_instance_two_dir_tree_model(Some(
two_instance_two_dir_components_model_missing_user(None)?,
))?;
let controller = RouteSourcesController::default();
let component_model = &data_model.get::<V2ComponentModel>()?.component_model;
let components = &data_model.get::<Components>()?.entries;
let config = RouteSourcesConfig {
component_routes: vec![RouteSourcesSpec {
target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
routes_to_skip: vec![],
routes_to_verify: vec![
// config.component_routes[0].routes_to_verify[0]:
// Match route: @root_url -> @two_dir_user_url route.
RouteMatch {
target: UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/root").unwrap()),
name: None,
},
source: SourceSpec {
node_path: NodePath::absolute_from_vec(vec![]),
capability: SourceDeclSpec {
// Match complete path with routed subdirs.
path_prefix: Some(
CapabilityPath::from_str(
"/data/to/user/root_subdir/user_subdir",
)
.unwrap(),
),
name: Some(CapabilityName("root_dir".to_string())),
},
},
},
// config.component_routes[0].routes_to_verify[1]:
// Match route:
// @one_dir_provider_url
// -> @root_url
// -> @one_dir_user_url.
RouteMatch {
target: UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/provider").unwrap()),
name: None,
},
source: SourceSpec {
node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
capability: SourceDeclSpec {
// Match complete path with routed subdirs.
path_prefix: Some(
CapabilityPath::from_str(
"/data/to/user/provider_subdir/root_subdir/user_subdir",
)
.unwrap(),
),
name: Some(CapabilityName("provider_dir".to_string())),
},
},
},
],
}],
};
let result = ok_unwrap!(controller.run(component_model, components, &config));
assert_eq!(
result,
hashmap! {
"/two_dir_user".to_string() => vec![
VerifyRouteSourcesResult{
query: config.component_routes[0].routes_to_verify[0].clone(),
result: Err(RouteSourceError::ComponentInstanceLookupByUrlFailed(make_test_url("two_dir_user").to_string())),
},
VerifyRouteSourcesResult{
query: config.component_routes[0].routes_to_verify[1].clone(),
result: Err(RouteSourceError::ComponentInstanceLookupByUrlFailed(make_test_url("two_dir_user").to_string())),
}
],
}
);
Ok(())
}
#[fuchsia::test]
fn test_verify_all_duplicate_user_component() -> Result<()> {
let (data_model, duplicate_components) =
two_instance_two_dir_components_model_duplicate_user(None)?;
let data_model = valid_two_instance_two_dir_tree_model(Some(data_model))?;
let controller = RouteSourcesController::default();
let component_model = &data_model.get::<V2ComponentModel>()?.component_model;
let components = &data_model.get::<Components>()?.entries;
let config = RouteSourcesConfig {
component_routes: vec![RouteSourcesSpec {
target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
routes_to_skip: vec![],
routes_to_verify: vec![
// config.component_routes[0].routes_to_verify[0]:
// Match route: @root_url -> @two_dir_user_url route.
RouteMatch {
target: UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/root").unwrap()),
name: None,
},
source: SourceSpec {
node_path: NodePath::absolute_from_vec(vec![]),
capability: SourceDeclSpec {
// Match complete path with routed subdirs.
path_prefix: Some(
CapabilityPath::from_str(
"/data/to/user/root_subdir/user_subdir",
)
.unwrap(),
),
name: Some(CapabilityName("root_dir".to_string())),
},
},
},
// config.component_routes[0].routes_to_verify[1]:
// Match route:
// @one_dir_provider_url
// -> @root_url
// -> @one_dir_user_url.
RouteMatch {
target: UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/provider").unwrap()),
name: None,
},
source: SourceSpec {
node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
capability: SourceDeclSpec {
// Match complete path with routed subdirs.
path_prefix: Some(
CapabilityPath::from_str(
"/data/to/user/provider_subdir/root_subdir/user_subdir",
)
.unwrap(),
),
name: Some(CapabilityName("provider_dir".to_string())),
},
},
},
],
}],
};
let result = ok_unwrap!(controller.run(component_model, components, &config));
assert_eq!(
result,
hashmap! {
"/two_dir_user".to_string() => vec![
VerifyRouteSourcesResult{
query: config.component_routes[0].routes_to_verify[0].clone(),
result: Err(RouteSourceError::MultipleComponentsWithSameUrl(duplicate_components.clone())),
},
VerifyRouteSourcesResult{
query: config.component_routes[0].routes_to_verify[1].clone(),
result: Err(RouteSourceError::MultipleComponentsWithSameUrl(duplicate_components)),
}
],
}
);
Ok(())
}
#[fuchsia::test]
fn test_verify_all_untrusted_user_source() -> Result<()> {
let (data_model, untrusted_source) =
two_instance_two_dir_components_model_untrusted_user_source(None)?;
let data_model = valid_two_instance_two_dir_tree_model(Some(data_model))?;
let controller = RouteSourcesController::default();
let component_model = &data_model.get::<V2ComponentModel>()?.component_model;
let components = &data_model.get::<Components>()?.entries;
let config = RouteSourcesConfig {
component_routes: vec![RouteSourcesSpec {
target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
routes_to_skip: vec![],
routes_to_verify: vec![
// config.component_routes[0].routes_to_verify[0]:
// Match route: @root_url -> @two_dir_user_url route.
RouteMatch {
target: UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/root").unwrap()),
name: None,
},
source: SourceSpec {
node_path: NodePath::absolute_from_vec(vec![]),
capability: SourceDeclSpec {
// Match complete path with routed subdirs.
path_prefix: Some(
CapabilityPath::from_str(
"/data/to/user/root_subdir/user_subdir",
)
.unwrap(),
),
name: Some(CapabilityName("root_dir".to_string())),
},
},
},
// config.component_routes[0].routes_to_verify[1]:
// Match route:
// @one_dir_provider_url
// -> @root_url
// -> @one_dir_user_url.
RouteMatch {
target: UseSpec {
type_name: CapabilityTypeName::Directory,
path: Some(CapabilityPath::from_str("/data/from/provider").unwrap()),
name: None,
},
source: SourceSpec {
node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
capability: SourceDeclSpec {
// Match complete path with routed subdirs.
path_prefix: Some(
CapabilityPath::from_str(
"/data/to/user/provider_subdir/root_subdir/user_subdir",
)
.unwrap(),
),
name: Some(CapabilityName("provider_dir".to_string())),
},
},
},
],
}],
};
let result = ok_unwrap!(controller.run(component_model, components, &config));
assert_eq!(
result,
hashmap! {
"/two_dir_user".to_string() => vec![
VerifyRouteSourcesResult{
query: config.component_routes[0].routes_to_verify[0].clone(),
result: Err(RouteSourceError::RouteSegmentComponentFromUntrustedSource(RouteSegment::UseBy {
node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
capability: UseDirectoryDecl{
source: UseSource::Parent,
source_name: CapabilityName("routed_from_root".to_string()),
target_path: CapabilityPath::from_str("/data/from/root").unwrap(),
rights: fio::Operations::CONNECT,
subdir: Some(PathBuf::from_str("user_subdir").unwrap()),
dependency_type: DependencyType::Strong,
availability: Availability::Required,
}.into(),
}, untrusted_source.clone())),
},
VerifyRouteSourcesResult{
query: config.component_routes[0].routes_to_verify[1].clone(),
result: Err(RouteSourceError::RouteSegmentComponentFromUntrustedSource(RouteSegment::UseBy {
node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
capability: UseDirectoryDecl{
source: UseSource::Parent,
source_name: CapabilityName("routed_from_provider".to_string()),
target_path: CapabilityPath::from_str("/data/from/provider").unwrap(),
rights: fio::Operations::CONNECT,
subdir: Some(PathBuf::from_str("user_subdir").unwrap()),
dependency_type: DependencyType::Strong,
availability: Availability::Required,
}.into(),
}, untrusted_source)),
}
],
}
);
Ok(())
}
}