blob: 343641e36e06d0f280be9bb7e1847ee960c8101f [file] [log] [blame]
// Copyright 2018 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::{
cml,
cml::CapabilityClause,
error::Error,
features::{Feature, FeatureSet},
one_or_many::OneOrMany,
util,
},
cm_json::{JsonSchema, CMX_SCHEMA},
cm_types::Name,
directed_graph::{self, DirectedGraph},
serde_json::Value,
std::{
collections::{BTreeMap, HashMap, HashSet},
fmt,
fs::File,
hash::Hash,
io::Read,
iter,
path::Path,
},
valico::json_schema,
};
/// Read in and parse one or more manifest files. Returns an Err() if any file is not valid
/// or Ok(()) if all files are valid.
///
/// The primary JSON schemas are taken from cm_json, selected based on the file extension,
/// is used to determine the validity of each input file. Extra schemas to validate against can be
/// optionally provided.
pub fn validate<P: AsRef<Path>>(
files: &[P],
extra_schemas: &[(P, Option<String>)],
features: &FeatureSet,
) -> Result<(), Error> {
if files.is_empty() {
return Err(Error::invalid_args("No files provided"));
}
for filename in files {
validate_file(filename.as_ref(), extra_schemas, features)?;
}
Ok(())
}
/// Validates a given cml.
pub fn validate_cml(
document: &cml::Document,
file: &Path,
features: &FeatureSet,
) -> Result<(), Error> {
let mut ctx = ValidationContext::new(&document, features);
let mut res = ctx.validate();
if let Err(Error::Validate { filename, .. }) = &mut res {
*filename = Some(file.to_string_lossy().into_owned());
}
res
}
/// Read in and parse a single manifest file, and return an Error if the given file is not valid.
fn validate_file<P: AsRef<Path>>(
file: &Path,
extra_schemas: &[(P, Option<String>)],
features: &FeatureSet,
) -> Result<(), Error> {
const BAD_EXTENSION: &str = "Input file does not have a component manifest extension \
(.cml or .cmx)";
// Validate based on file extension.
let ext = file.extension().and_then(|e| e.to_str());
match ext {
Some("cmx") => {
let mut buffer = String::new();
File::open(&file)?.read_to_string(&mut buffer)?;
let v = serde_json::from_str(&buffer)?;
validate_json(&v, CMX_SCHEMA)?;
// Validate against any extra schemas provided.
for extra_schema in extra_schemas {
let schema = JsonSchema::new_from_file(&extra_schema.0.as_ref())?;
validate_json(&v, &schema).map_err(|e| match (&e, &extra_schema.1) {
(Error::Validate { schema_name, err, filename }, Some(extra_msg)) => {
Error::Validate {
schema_name: schema_name.clone(),
err: format!("{}\n{}", err, extra_msg),
filename: filename.clone(),
}
}
_ => e,
})?;
}
}
Some("cml") => {
let document = util::read_cml(file)?;
validate_cml(&document, &file, features)?;
}
_ => {
return Err(Error::invalid_args(BAD_EXTENSION));
}
};
Ok(())
}
/// Validates a JSON document according to the given schema.
pub fn validate_json(json: &Value, schema: &JsonSchema<'_>) -> Result<(), Error> {
// Parse the schema
let cmx_schema_json = serde_json::from_str(&schema.schema).map_err(|e| {
Error::internal(format!("Couldn't read schema '{}' as JSON: {}", schema.name, e))
})?;
let mut scope = json_schema::Scope::new();
let compiled_schema = scope.compile_and_return(cmx_schema_json, false).map_err(|e| {
Error::internal(format!("Couldn't parse schema '{}': {:?}", schema.name, e))
})?;
// Validate the json
let res = compiled_schema.validate(json);
if !res.is_strictly_valid() {
let mut err_msgs = Vec::new();
for e in &res.errors {
err_msgs.push(format!("{} at {}", e.get_title(), e.get_path()).into_boxed_str());
}
for u in &res.missing {
err_msgs.push(
format!("internal error: schema definition is missing URL {}", u).into_boxed_str(),
);
}
// The ordering in which valico emits these errors is unstable.
// Sort error messages so that the resulting message is predictable.
err_msgs.sort_unstable();
return Err(Error::validate_schema(&schema.name.to_string(), err_msgs.join(", ")));
}
Ok(())
}
struct ValidationContext<'a> {
document: &'a cml::Document,
features: &'a FeatureSet,
all_children: HashMap<&'a cml::Name, &'a cml::Child>,
all_collections: HashSet<&'a cml::Name>,
all_storage_and_sources: HashMap<&'a cml::Name, &'a cml::CapabilityFromRef>,
all_services: HashSet<&'a cml::Name>,
all_protocols: HashSet<&'a cml::Name>,
all_directories: HashSet<&'a cml::Name>,
all_runners: HashSet<&'a cml::Name>,
all_resolvers: HashSet<&'a cml::Name>,
all_environment_names: HashSet<&'a cml::Name>,
all_event_names: HashSet<cml::Name>,
all_capability_names: HashSet<cml::Name>,
}
impl<'a> ValidationContext<'a> {
fn new(document: &'a cml::Document, features: &'a FeatureSet) -> Self {
ValidationContext {
document,
features,
all_children: HashMap::new(),
all_collections: HashSet::new(),
all_storage_and_sources: HashMap::new(),
all_services: HashSet::new(),
all_protocols: HashSet::new(),
all_directories: HashSet::new(),
all_runners: HashSet::new(),
all_resolvers: HashSet::new(),
all_environment_names: HashSet::new(),
all_event_names: HashSet::new(),
all_capability_names: HashSet::new(),
}
}
fn validate(&mut self) -> Result<(), Error> {
// Ensure child components, collections, and storage don't use the
// same name.
//
// We currently have the ability to distinguish between storage and
// children/collections based on context, but still enforce name
// uniqueness to give us flexibility in future.
let all_children_names =
self.document.all_children_names().into_iter().zip(iter::repeat("children"));
let all_collection_names =
self.document.all_collection_names().into_iter().zip(iter::repeat("collections"));
let all_storage_names =
self.document.all_storage_names().into_iter().zip(iter::repeat("storage"));
let all_runner_names =
self.document.all_runner_names().into_iter().zip(iter::repeat("runners"));
let all_resolver_names =
self.document.all_resolver_names().into_iter().zip(iter::repeat("resolvers"));
let all_environment_names =
self.document.all_environment_names().into_iter().zip(iter::repeat("environments"));
ensure_no_duplicate_names(
all_children_names
.chain(all_collection_names)
.chain(all_storage_names)
.chain(all_runner_names)
.chain(all_resolver_names)
.chain(all_environment_names),
)?;
// Populate the sets of children and collections.
if let Some(children) = &self.document.children {
self.all_children = children.iter().map(|c| (&c.name, c)).collect();
}
self.all_collections = self.document.all_collection_names().into_iter().collect();
self.all_storage_and_sources = self.document.all_storage_and_sources();
self.all_services = self.document.all_service_names().into_iter().collect();
self.all_protocols = self.document.all_protocol_names().into_iter().collect();
self.all_directories = self.document.all_directory_names().into_iter().collect();
self.all_runners = self.document.all_runner_names().into_iter().collect();
self.all_resolvers = self.document.all_resolver_names().into_iter().collect();
self.all_environment_names = self.document.all_environment_names().into_iter().collect();
self.all_event_names = self.document.all_event_names()?.into_iter().collect();
self.all_capability_names = self.document.all_capability_names();
// Validate "children".
let mut strong_dependencies = DirectedGraph::new();
if let Some(children) = &self.document.children {
for child in children {
self.validate_child(&child, &mut strong_dependencies)?;
}
}
// Validate "collections".
if let Some(collections) = &self.document.collections {
for collection in collections {
self.validate_collection(&collection, &mut strong_dependencies)?;
}
}
// Validate "capabilities".
if let Some(capabilities) = self.document.capabilities.as_ref() {
let mut used_ids = HashMap::new();
for capability in capabilities {
self.validate_capability(capability, &mut used_ids)?;
}
}
// Validate "use".
if let Some(uses) = self.document.r#use.as_ref() {
let mut used_ids = HashMap::new();
for use_ in uses.iter() {
self.validate_use(&use_, &mut used_ids, &mut strong_dependencies)?;
}
}
// Validate "expose".
if let Some(exposes) = self.document.expose.as_ref() {
let mut used_ids = HashMap::new();
for expose in exposes.iter() {
self.validate_expose(&expose, &mut used_ids)?;
}
}
// Validate "offer".
if let Some(offers) = self.document.offer.as_ref() {
let mut used_ids = HashMap::new();
for offer in offers.iter() {
self.validate_offer(&offer, &mut used_ids, &mut strong_dependencies)?;
}
}
// Ensure we don't have a component with a "program" block which fails to specify a runner.
self.validate_runner_specified(self.document.program.as_ref())?;
// Validate "environments".
if let Some(environments) = &self.document.environments {
for env in environments {
self.validate_environment(&env, &mut strong_dependencies)?;
}
}
// Validate "config"
if let Some(config) = &self.document.config {
self.validate_config(config)?;
}
// Check for dependency cycles
match strong_dependencies.topological_sort() {
Ok(_) => {}
Err(e) => {
return Err(Error::validate(format!(
"Strong dependency cycles were found. Break the cycle by removing a dependency or marking an offer as weak. Cycles: {}", e.format_cycle())));
}
}
Ok(())
}
fn validate_child(
&self,
child: &'a cml::Child,
strong_dependencies: &mut DirectedGraph<DependencyNode<'a>>,
) -> Result<(), Error> {
if let Some(environment_ref) = &child.environment {
match environment_ref {
cml::EnvironmentRef::Named(environment_name) => {
if !self.all_environment_names.contains(&environment_name) {
return Err(Error::validate(format!(
"\"{}\" does not appear in \"environments\"",
&environment_name
)));
}
let source = DependencyNode::Named(&environment_name);
let target = DependencyNode::Named(&child.name);
self.add_strong_dep(None, source, target, strong_dependencies);
}
}
}
Ok(())
}
fn validate_collection(
&self,
collection: &'a cml::Collection,
strong_dependencies: &mut DirectedGraph<DependencyNode<'a>>,
) -> Result<(), Error> {
if collection.allowed_offers.is_some() {
self.features.check(Feature::DynamicOffers)?;
}
if collection.allow_long_names.is_some() {
self.features.check(Feature::AllowLongNames)?;
}
if let Some(environment_ref) = &collection.environment {
match environment_ref {
cml::EnvironmentRef::Named(environment_name) => {
if !self.all_environment_names.contains(&environment_name) {
return Err(Error::validate(format!(
"\"{}\" does not appear in \"environments\"",
&environment_name
)));
}
let source = DependencyNode::Named(&environment_name);
let target = DependencyNode::Named(&collection.name);
self.add_strong_dep(None, source, target, strong_dependencies);
}
}
}
Ok(())
}
fn validate_capability(
&self,
capability: &'a cml::Capability,
used_ids: &mut HashMap<String, cml::CapabilityId>,
) -> Result<(), Error> {
if capability.directory.is_some() && capability.path.is_none() {
return Err(Error::validate("\"path\" should be present with \"directory\""));
}
if capability.directory.is_some() && capability.rights.is_none() {
return Err(Error::validate("\"rights\" should be present with \"directory\""));
}
if capability.storage.is_some() {
if capability.from.is_none() {
return Err(Error::validate("\"from\" should be present with \"storage\""));
}
if capability.path.is_some() {
return Err(Error::validate(
"\"path\" can not be present with \"storage\", use \"backing_dir\"",
));
}
if capability.backing_dir.is_none() {
return Err(Error::validate("\"backing_dir\" should be present with \"storage\""));
}
if capability.storage_id.is_none() {
return Err(Error::validate("\"storage_id\" should be present with \"storage\""));
}
}
if capability.runner.is_some() && capability.from.is_some() {
return Err(Error::validate("\"from\" should not be present with \"runner\""));
}
if capability.runner.is_some() && capability.path.is_none() {
return Err(Error::validate("\"path\" should be present with \"runner\""));
}
if capability.resolver.is_some() && capability.from.is_some() {
return Err(Error::validate("\"from\" should not be present with \"resolver\""));
}
if capability.resolver.is_some() && capability.path.is_none() {
return Err(Error::validate("\"path\" should be present with \"resolver\""));
}
if let Some(from) = capability.from.as_ref() {
self.validate_component_child_ref("\"capabilities\" source", &cml::AnyRef::from(from))?;
}
// Disallow multiple capability ids of the same name.
let capability_ids = cml::CapabilityId::from_capability(capability)?;
for capability_id in capability_ids {
if used_ids.insert(capability_id.to_string(), capability_id.clone()).is_some() {
return Err(Error::validate(format!(
"\"{}\" is a duplicate \"capability\" name",
capability_id,
)));
}
}
Ok(())
}
fn validate_use(
&self,
use_: &'a cml::Use,
used_ids: &mut HashMap<String, cml::CapabilityId>,
strong_dependencies: &mut DirectedGraph<DependencyNode<'a>>,
) -> Result<(), Error> {
if use_.service.is_some() {
if use_.r#as.is_some() {
return Err(Error::validate("\"as\" cannot be used with \"service\""));
}
}
if use_.from == Some(cml::UseFromRef::Debug) && use_.protocol.is_none() {
return Err(Error::validate("only \"protocol\" supports source from \"debug\""));
}
if use_.protocol.is_some() && use_.r#as.is_some() {
return Err(Error::validate("\"as\" cannot be used with \"protocol\""));
}
if use_.directory.is_some() && use_.r#as.is_some() {
return Err(Error::validate("\"as\" cannot be used with \"directory\""));
}
if use_.event.is_some() && use_.from.is_none() {
return Err(Error::validate("\"from\" should be present with \"event\""));
}
if use_.event_stream.is_some() && use_.from.is_none() {
return Err(Error::validate("\"from\" should be present with \"event_stream\""));
}
if use_.event.is_none() && use_.filter.is_some() {
return Err(Error::validate("\"filter\" can only be used with \"event\""));
}
if use_.storage.is_some() && use_.from.is_some() {
return Err(Error::validate("\"from\" cannot be used with \"storage\""));
}
if use_.storage.is_some() && use_.r#as.is_some() {
return Err(Error::validate("\"as\" cannot be used with \"storage\""));
}
if use_.from == Some(cml::UseFromRef::Self_) && use_.event.is_some() {
return Err(Error::validate("\"from: self\" cannot be used with \"event\""));
}
if use_.from == Some(cml::UseFromRef::Self_) && use_.event_stream.is_some() {
return Err(Error::validate("\"from: self\" cannot be used with \"event_stream\""));
}
if use_.availability == Some(cml::Availability::SameAsTarget) {
return Err(Error::validate(
"\"availability: same_as_target\" cannot be used with use declarations",
));
}
if let Some(source) = DependencyNode::use_from_ref(use_.from.as_ref()) {
for name in &use_.names() {
let target = DependencyNode::Self_;
if use_.dependency.as_ref().unwrap_or(&cml::DependencyType::Strong)
== &cml::DependencyType::Strong
{
self.add_strong_dep(Some(name), source, target, strong_dependencies);
}
}
}
match (use_.event_stream_deprecated.as_ref(), use_.subscriptions.as_ref()) {
(Some(_), Some(subscriptions)) => {
let event_names = subscriptions
.iter()
.map(|subscription| subscription.event.to_vec())
.flatten()
.collect::<Vec<&Name>>();
let mut unique_event_names = HashSet::new();
for event_name in event_names {
if !unique_event_names.insert(event_name) {
return Err(Error::validate(format!(
"Event \"{}\" is duplicated in event stream subscriptions.",
event_name,
)));
}
if !self.all_event_names.contains(event_name) {
return Err(Error::validate(format!(
"Event \"{}\" in event stream not found in any \"use\" declaration.",
event_name
)));
}
}
}
(None, Some(_)) => {
return Err(Error::validate("\"event_stream\" must be named."));
}
(Some(_), None) => {
return Err(Error::validate("\"event_stream\" must have subscriptions."));
}
(None, None) => {}
}
// Disallow multiple capability ids of the same name.
let capability_ids = cml::CapabilityId::from_use(use_)?;
for capability_id in capability_ids {
if used_ids.insert(capability_id.to_string(), capability_id.clone()).is_some() {
return Err(Error::validate(format!(
"\"{}\" is a duplicate \"use\" target {}",
capability_id,
capability_id.type_str()
)));
}
let dir = match capability_id.get_dir_path() {
Some(d) => d,
None => continue,
};
// Validate that paths-based capabilities (service, directory, protocol)
// are not prefixes of each other.
for (_, used_id) in used_ids.iter() {
if capability_id == *used_id {
continue;
}
let used_dir = match used_id.get_dir_path() {
Some(d) => d,
None => continue,
};
if match (used_id, &capability_id) {
// Directories and storage can't be the same or partially overlap.
(cml::CapabilityId::UsedDirectory(_), cml::CapabilityId::UsedStorage(_))
| (cml::CapabilityId::UsedStorage(_), cml::CapabilityId::UsedDirectory(_))
| (cml::CapabilityId::UsedDirectory(_), cml::CapabilityId::UsedDirectory(_))
| (cml::CapabilityId::UsedStorage(_), cml::CapabilityId::UsedStorage(_)) => {
dir == used_dir || dir.starts_with(used_dir) || used_dir.starts_with(dir)
}
// Protocols and services can't overlap with directories or storage.
(cml::CapabilityId::UsedDirectory(_), _)
| (cml::CapabilityId::UsedStorage(_), _)
| (_, cml::CapabilityId::UsedDirectory(_))
| (_, cml::CapabilityId::UsedStorage(_)) => {
dir == used_dir || dir.starts_with(used_dir) || used_dir.starts_with(dir)
}
// Protocols and services containing directories may be same, but
// partial overlap is disallowed.
(_, _) => {
dir != used_dir && (dir.starts_with(used_dir) || used_dir.starts_with(dir))
}
} {
return Err(Error::validate(format!(
"{} \"{}\" is a prefix of \"use\" target {} \"{}\"",
used_id.type_str(),
used_id,
capability_id.type_str(),
capability_id,
)));
}
}
}
if let Some(directory) = use_.directory.as_ref() {
if directory.as_str() == "hub" && use_.from == Some(cml::UseFromRef::Framework) {
self.features.check(Feature::Hub)?;
}
// All directory "use" expressions must have directory rights.
match &use_.rights {
Some(rights) => self.validate_directory_rights(&rights)?,
None => return Err(Error::validate("Rights required for this use statement.")),
};
}
match (&use_.from, &use_.dependency) {
(Some(cml::UseFromRef::Named(name)), _) => {
self.validate_component_child_or_capability_ref(
"\"use\" source",
&cml::AnyRef::Named(name),
)?;
}
(_, Some(cml::DependencyType::Weak) | Some(cml::DependencyType::WeakForMigration)) => {
return Err(Error::validate(format!(
"Only `use` from children can have dependency: \"weak\""
)));
}
_ => {}
}
Ok(())
}
fn validate_expose(
&self,
expose: &'a cml::Expose,
used_ids: &mut HashMap<String, cml::CapabilityId>,
) -> Result<(), Error> {
// TODO: Many of these checks are similar, see if we can unify them
// Ensure that if the expose target is framework, the source target is self always.
if expose.to == Some(cml::ExposeToRef::Framework) {
match &expose.from {
OneOrMany::One(cml::ExposeFromRef::Self_) => {}
OneOrMany::Many(vec)
if vec.iter().all(|from| *from == cml::ExposeFromRef::Self_) => {}
_ => {
return Err(Error::validate("Expose to framework can only be done from self."))
}
}
}
// Ensure that services exposed from self are defined in `capabilities`.
if let Some(service) = expose.service.as_ref() {
for service in service {
if expose.from.iter().any(|r| *r == cml::ExposeFromRef::Self_) {
if !self.all_services.contains(service) {
return Err(Error::validate(format!(
"Service \"{}\" is exposed from self, so it must be declared as a \"service\" in \"capabilities\"",
service
)));
}
}
}
}
// Ensure that protocols exposed from self are defined in `capabilities`.
if let Some(protocol) = expose.protocol.as_ref() {
for protocol in protocol {
if expose.from.iter().any(|r| *r == cml::ExposeFromRef::Self_) {
if !self.all_protocols.contains(protocol) {
return Err(Error::validate(format!(
"Protocol \"{}\" is exposed from self, so it must be declared as a \"protocol\" in \"capabilities\"",
protocol
)));
}
}
}
}
if let Some(directory) = expose.directory.as_ref() {
for directory in directory {
if directory.as_str() == "hub"
&& expose.from.iter().any(|r| *r == cml::ExposeFromRef::Framework)
{
{
self.features.check(Feature::Hub)?;
}
}
// Ensure that directories exposed from self are defined in `capabilities`.
if expose.from.iter().any(|r| *r == cml::ExposeFromRef::Self_) {
if !self.all_directories.contains(directory) {
return Err(Error::validate(format!(
"Directory \"{}\" is exposed from self, so it must be declared as a \"directory\" in \"capabilities\"",
directory
)));
}
}
}
}
// Ensure directory rights are valid.
if let Some(_) = expose.directory.as_ref() {
if expose.from.iter().any(|r| *r == cml::ExposeFromRef::Self_)
|| expose.rights.is_some()
{
if let Some(rights) = expose.rights.as_ref() {
self.validate_directory_rights(&rights)?;
}
}
// Exposing a subdirectory makes sense for routing but when exposing to framework,
// the subdir should be exposed directly.
if expose.to == Some(cml::ExposeToRef::Framework) {
if expose.subdir.is_some() {
return Err(Error::validate(
"`subdir` is not supported for expose to framework. Directly expose the subdirectory instead."
));
}
}
}
// Ensure that runners exposed from self are defined in `capabilities`.
if let Some(runner) = expose.runner.as_ref() {
for runner in runner {
if expose.from.iter().any(|r| *r == cml::ExposeFromRef::Self_) {
if !self.all_runners.contains(runner) {
return Err(Error::validate(format!(
"Runner \"{}\" is exposed from self, so it must be declared as a \"runner\" in \"capabilities\"",
runner
)));
}
}
}
}
// Ensure that resolvers exposed from self are defined in `capabilities`.
if let Some(resolver) = expose.resolver.as_ref() {
for resolver in resolver {
if expose.from.iter().any(|r| *r == cml::ExposeFromRef::Self_) {
if !self.all_resolvers.contains(resolver) {
return Err(Error::validate(format!(
"Resolver \"{}\" is exposed from self, so it must be declared as a \"resolver\" in \"capabilities\"", resolver
)));
}
}
}
}
if let Some(event_stream) = &expose.event_stream {
if event_stream.iter().len() > 1 && expose.r#as.is_some() {
return Err(Error::validate(format!("as cannot be used with multiple events")));
}
if let Some(cml::ExposeToRef::Framework) = &expose.to {
return Err(Error::validate(format!("cannot expose an event_stream to framework")));
}
for from in expose.from.iter() {
if from == &cml::ExposeFromRef::Self_ {
return Err(Error::validate(format!("Cannot expose event_streams from self")));
}
}
if let Some(scopes) = &expose.scope {
for scope in scopes {
match scope {
cml::EventScope::Named(name) => {
if !self.all_children.contains_key(name)
&& !self.all_collections.contains(name)
{
return Err(Error::validate(format!("event_stream scope {} did not match a component or collection in this .cml file.", name.as_str())));
}
}
}
}
}
}
// Ensure we haven't already exposed an entity of the same name.
let capability_ids = cml::CapabilityId::from_offer_expose(expose)?;
for capability_id in capability_ids {
if used_ids.insert(capability_id.to_string(), capability_id.clone()).is_some() {
return Err(Error::validate(format!(
"\"{}\" is a duplicate \"expose\" target capability for \"{}\"",
capability_id,
expose.to.as_ref().unwrap_or(&cml::ExposeToRef::Parent)
)));
}
}
// Validate `from` (done last because this validation depends on the capability type, which
// must be validated first)
self.validate_from_clause("expose", expose, &None, &None)?;
Ok(())
}
fn validate_offer(
&self,
offer: &'a cml::Offer,
used_ids: &mut HashMap<&'a cml::Name, HashMap<String, cml::CapabilityId>>,
strong_dependencies: &mut DirectedGraph<DependencyNode<'a>>,
) -> Result<(), Error> {
// TODO: Many of these checks are repititious, see if we can unify them
// Ensure that services offered from self are defined in `services`.
if let Some(service) = offer.service.as_ref() {
for service in service {
if offer.from.iter().any(|r| *r == cml::OfferFromRef::Self_) {
if !self.all_services.contains(service) {
return Err(Error::validate(format!(
"Service \"{}\" is offered from self, so it must be declared as a \
\"service\" in \"capabilities\"",
service
)));
}
}
}
}
// Ensure that protocols offered from self are defined in `capabilities`.
if let Some(protocol) = offer.protocol.as_ref() {
for protocol in protocol {
if offer.from.iter().any(|r| *r == cml::OfferFromRef::Self_) {
if !self.all_protocols.contains(protocol) {
return Err(Error::validate(format!(
"Protocol \"{}\" is offered from self, so it must be declared as a \"protocol\" in \"capabilities\"",
protocol
)));
}
}
}
}
if let Some(stream) = offer.event_stream.as_ref() {
if stream.iter().len() > 1 && offer.r#as.is_some() {
return Err(Error::validate(format!("as cannot be used with multiple events")));
}
for from in &offer.from {
match from {
cml::OfferFromRef::Self_ => {
return Err(Error::validate(format!(
"cannot offer an event_stream from self"
)));
}
_ => {}
}
}
}
if let Some(directory) = offer.directory.as_ref() {
for directory in directory {
if directory.as_str() == "hub"
&& offer.from.iter().any(|r| *r == cml::OfferFromRef::Framework)
{
self.features.check(Feature::Hub)?;
}
// Ensure that directories offered from self are defined in `capabilities`.
if offer.from.iter().any(|r| *r == cml::OfferFromRef::Self_) {
if !self.all_directories.contains(directory) {
return Err(Error::validate(format!(
"Directory \"{}\" is offered from self, so it must be declared as a \"directory\" in \"capabilities\"",
directory
)));
}
}
}
}
// Ensure directory rights are valid.
if let Some(_) = offer.directory.as_ref() {
if offer.from.iter().any(|r| *r == cml::OfferFromRef::Self_) || offer.rights.is_some() {
if let Some(rights) = offer.rights.as_ref() {
self.validate_directory_rights(&rights)?;
}
}
}
// Ensure that storage offered from self are defined in `capabilities`.
if let Some(storage) = offer.storage.as_ref() {
for storage in storage {
if offer.from.iter().any(|r| r.is_named()) {
return Err(Error::validate(format!(
"Storage \"{}\" is offered from a child, but storage capabilities cannot be exposed", storage)));
}
if offer.from.iter().any(|r| *r == cml::OfferFromRef::Self_) {
if !self.all_storage_and_sources.contains_key(storage) {
return Err(Error::validate(format!(
"Storage \"{}\" is offered from self, so it must be declared as a \"storage\" in \"capabilities\"",
storage
)));
}
}
}
}
// Ensure that runners offered from self are defined in `runners`.
if let Some(runner) = offer.runner.as_ref() {
for runner in runner {
if offer.from.iter().any(|r| *r == cml::OfferFromRef::Self_) {
if !self.all_runners.contains(runner) {
return Err(Error::validate(format!(
"Runner \"{}\" is offered from self, so it must be declared as a \
\"runner\" in \"capabilities\"",
runner
)));
}
}
}
}
// Ensure that resolvers offered from self are defined in `resolvers`.
if let Some(resolver) = offer.resolver.as_ref() {
for resolver in resolver {
if offer.from.iter().any(|r| *r == cml::OfferFromRef::Self_) {
if !self.all_resolvers.contains(resolver) {
return Err(Error::validate(format!(
"Resolver \"{}\" is offered from self, so it must be declared as a \
\"resolver\" in \"capabilities\"",
resolver
)));
}
}
}
}
// Ensure that dependency can only be provided for directories and protocols
if offer.dependency.is_some() && offer.directory.is_none() && offer.protocol.is_none() {
return Err(Error::validate(
"Dependency can only be provided for protocol and directory capabilities",
));
}
// Ensure that only events can have filter.
match (&offer.event, &offer.event_stream, &offer.filter) {
(None, None, Some(_)) => Err(Error::validate(
"\"filter\" can only be used with \"event\" or \"event_stream\"",
)),
(None, Some(OneOrMany::Many(_)), Some(_)) => {
Err(Error::validate("\"filter\" cannot be used with multiple events."))
}
_ => Ok(()),
}?;
if let Some(event_stream) = &offer.event_stream {
for from in &offer.from {
match (from, &offer.filter) {
(cml::OfferFromRef::Framework, Some(_)) => {
for event in event_stream {
match event.as_str() {
"capability_requested"=>{},
"directory_ready"=>{},
_=>{
return Err(Error::validate("\"filter\" can only be used when offering \"capability_requested\" or \"directory_ready\" from framework."))
}
}
}
}
(cml::OfferFromRef::Framework, None) => {
for event in event_stream {
match event.as_str() {
"capability_requested" | "directory_ready"=>{
return Err(Error::validate("\"filter\" must be specified if \"capability_requested\" or \"directory_ready\" are offered from framework"))
},
_=>{},
}
}
}
_ => {}
}
}
}
// Validate every target of this offer.
let target_cap_ids = cml::CapabilityId::from_offer_expose(offer)?;
for to in &offer.to {
// Ensure the "to" value is a child.
let to_target = match to {
cml::OfferToRef::Named(ref name) => name,
};
// Check that any referenced child actually exists.
if !self.all_children.contains_key(to_target)
&& !self.all_collections.contains(to_target)
{
return Err(Error::validate(format!(
"\"{}\" is an \"offer\" target but it does not appear in \"children\" \
or \"collections\"",
to
)));
}
// Ensure that a target is not offered more than once.
let ids_for_entity = used_ids.entry(to_target).or_insert(HashMap::new());
for target_cap_id in &target_cap_ids {
if ids_for_entity.insert(target_cap_id.to_string(), target_cap_id.clone()).is_some()
{
return Err(Error::validate(format!(
"\"{}\" is a duplicate \"offer\" target capability for \"{}\"",
target_cap_id, to
)));
}
}
// Ensure we are not offering a capability back to its source.
if let Some(storage) = offer.storage.as_ref() {
for storage in storage {
// Storage can only have a single `from` clause and this has been
// verified.
if let OneOrMany::One(cml::OfferFromRef::Self_) = &offer.from {
if let Some(cml::CapabilityFromRef::Named(source)) =
self.all_storage_and_sources.get(storage)
{
if to_target == source {
return Err(Error::validate(format!(
"Storage offer target \"{}\" is same as source",
to
)));
}
}
}
}
} else {
for reference in offer.from.to_vec() {
match reference {
cml::OfferFromRef::Named(name) if name == to_target => {
return Err(Error::validate(format!(
"Offer target \"{}\" is same as source",
to
)));
}
_ => {}
}
}
}
// Collect strong dependencies. We'll check for dependency cycles after all offer
// declarations are validated.
for from in offer.from.to_vec().iter() {
let is_strong = if offer.directory.is_some() || offer.protocol.is_some() {
offer.dependency.as_ref().unwrap_or(&cml::DependencyType::Strong)
== &cml::DependencyType::Strong
} else {
true
};
if is_strong {
if let Some(source) = DependencyNode::offer_from_ref(from) {
for name in &offer.names() {
let target = DependencyNode::offer_to_ref(to);
self.add_strong_dep(Some(name), source, target, strong_dependencies);
}
}
if let cml::OfferFromRef::Named(from) = from {
match to {
cml::OfferToRef::Named(to) => {
let source = DependencyNode::Named(from);
let target = DependencyNode::Named(to);
for name in &offer.names() {
self.add_strong_dep(
Some(name),
source,
target,
strong_dependencies,
);
}
}
}
}
}
}
}
// Validate `from` (done last because this validation depends on the capability type, which
// must be validated first)
self.validate_from_clause("offer", offer, &offer.source_availability, &offer.availability)?;
Ok(())
}
/// Adds a strong dependency between two nodes in the dependency graph between `source` and
/// `target`.
///
/// `name` is the name of the capability being routed (if applicable).
fn add_strong_dep(
&self,
name: Option<&cml::Name>,
source: DependencyNode<'a>,
target: DependencyNode<'a>,
strong_dependencies: &mut DirectedGraph<DependencyNode<'a>>,
) {
let source = {
// A dependency on a storage capability from `self` is really a dependency on the
// backing dir. Perform that translation here.
let possible_storage_name = match (source, name) {
(DependencyNode::Named(name), _) => Some(name),
(DependencyNode::Self_, Some(name)) => Some(name),
_ => None,
};
let possible_storage_source =
possible_storage_name.map(|name| self.all_storage_and_sources.get(&name)).flatten();
let source = possible_storage_source
.map(|r| DependencyNode::capability_from_ref(r))
.unwrap_or(Some(source));
if source.is_none() {
return;
}
source.unwrap()
};
if source == DependencyNode::Self_ && target == DependencyNode::Self_ {
// `self` dependencies (e.g. `use from self`) are allowed.
} else {
strong_dependencies.add_edge(source, target);
}
}
/// Validates that the from clause:
///
/// - is applicable to the capability type,
/// - does not contain duplicates,
/// - references names that exist.
/// - has availability "optional" if the source is "void"
///
/// `verb` is used in any error messages and is expected to be "offer", "expose", etc.
fn validate_from_clause<T>(
&self,
verb: &str,
cap: &T,
source_availability: &Option<cml::SourceAvailability>,
availability: &Option<cml::Availability>,
) -> Result<(), Error>
where
T: cml::CapabilityClause + cml::FromClause,
{
let from = cap.from_();
if cap.service().is_none() && from.is_many() {
return Err(Error::validate(format!(
"\"{}\" capabilities cannot have multiple \"from\" clauses",
cap.capability_type()
)));
}
if from.is_many() {
ensure_no_duplicate_values(&cap.from_())?;
}
let reference_description = format!("\"{}\" source", verb);
for from_clause in from {
// If this is a protocol, it could reference either a child or a storage capability
// (for the storage admin protocol).
let ref_validity_res = if cap.protocol().is_some() {
self.validate_component_child_or_capability_ref(
&reference_description,
&from_clause,
)
} else if cap.service().is_some() {
// Services can also be sourced from collections.
self.validate_component_child_or_collection_ref(
&reference_description,
&from_clause,
)
} else {
self.validate_component_child_ref(&reference_description, &from_clause)
};
match ref_validity_res {
Ok(()) if from_clause == cml::AnyRef::Void => {
// The source is valid and void
if availability != &Some(cml::Availability::Optional) {
return Err(Error::validate(format!(
"capabilities with a source of \"void\" must have an availability of \"optional\"",
)));
}
}
Ok(()) => {
// The source is valid and not void.
}
Err(_) if source_availability == &Some(cml::SourceAvailability::Unknown) => {
// The source is invalid, and will be rewritten to void
if availability != &Some(cml::Availability::Optional) && availability != &None {
return Err(Error::validate(format!(
"capabilities with an intentionally missing source must have an availability that is either unset or \"optional\"",
)));
}
}
Err(e) => {
// The source is invalid, but we're expecting it to be valid.
return Err(e);
}
}
}
Ok(())
}
/// Validates that the given component exists.
///
/// - `reference_description` is a human-readable description of the reference used in error
/// message, such as `"offer" source`.
/// - `component_ref` is a reference to a component. If the reference is a named child, we
/// ensure that the child component exists.
fn validate_component_child_ref(
&self,
reference_description: &str,
component_ref: &cml::AnyRef,
) -> Result<(), Error> {
match component_ref {
cml::AnyRef::Named(name) => {
// Ensure we have a child defined by that name.
if !self.all_children.contains_key(name) {
return Err(Error::validate(format!(
"{} \"{}\" does not appear in \"children\"",
reference_description, component_ref
)));
}
Ok(())
}
// We don't attempt to validate other reference types.
_ => Ok(()),
}
}
/// Validates that the given component/collection exists.
///
/// - `reference_description` is a human-readable description of the reference used in error
/// message, such as `"offer" source`.
/// - `component_ref` is a reference to a component/collection. If the reference is a named
/// child or collection, we ensure that the child component/collection exists.
fn validate_component_child_or_collection_ref(
&self,
reference_description: &str,
component_ref: &cml::AnyRef,
) -> Result<(), Error> {
match component_ref {
cml::AnyRef::Named(name) => {
// Ensure we have a child defined by that name.
if !self.all_children.contains_key(name) && !self.all_collections.contains(name) {
return Err(Error::validate(format!(
"{} \"{}\" does not appear in \"children\" or \"collections\"",
reference_description, component_ref
)));
}
Ok(())
}
// We don't attempt to validate other reference types.
_ => Ok(()),
}
}
/// Validates that the given capability exists.
///
/// - `reference_description` is a human-readable description of the reference used in error
/// message, such as `"offer" source`.
/// - `capability_ref` is a reference to a capability. If the reference is a named capability,
/// we ensure that the capability exists.
fn validate_component_capability_ref(
&self,
reference_description: &str,
capability_ref: &cml::AnyRef,
) -> Result<(), Error> {
match capability_ref {
cml::AnyRef::Named(name) => {
if !self.all_capability_names.contains(name) {
return Err(Error::validate(format!(
"{} \"{}\" does not appear in \"capabilities\"",
reference_description, capability_ref
)));
}
Ok(())
}
_ => Ok(()),
}
}
/// Validates that the given child component or capability exists.
///
/// - `reference_description` is a human-readable description of the reference used in error
/// message, such as `"offer" source`.
/// - `ref_` is a reference to a child component or capability. If the reference contains a
/// name, we ensure that a child component or a capability with the name exists.
fn validate_component_child_or_capability_ref(
&self,
reference_description: &str,
ref_: &cml::AnyRef,
) -> Result<(), Error> {
if self.validate_component_child_ref(reference_description, ref_).is_err()
&& self.validate_component_capability_ref(reference_description, ref_).is_err()
{
return Err(Error::validate(format!(
"{} \"{}\" does not appear in \"children\" or \"capabilities\"",
reference_description, ref_
)));
}
Ok(())
}
/// Validates that directory rights for all route types are valid, i.e that it does not
/// contain duplicate rights.
fn validate_directory_rights(&self, rights_clause: &cml::Rights) -> Result<(), Error> {
let mut rights = HashSet::new();
for right_token in rights_clause.0.iter() {
for right in right_token.expand() {
if !rights.insert(right) {
return Err(Error::validate(format!(
"\"{}\" is duplicated in the rights clause.",
right_token
)));
}
}
}
Ok(())
}
/// Ensure we don't have a component with a "program" block which fails to specify a runner.
fn validate_runner_specified(&self, program: Option<&cml::Program>) -> Result<(), Error> {
match program {
Some(program) => match program.runner {
Some(_) => Ok(()),
None => Err(Error::validate(
"Component has a `program` block defined, but doesn't specify a `runner`. \
Components need to use a runner to actually execute code.",
)),
},
None => Ok(()),
}
}
fn validate_config(
&self,
fields: &BTreeMap<cml::ConfigKey, cml::ConfigValueType>,
) -> Result<(), Error> {
self.features.check(Feature::StructuredConfig)?;
if fields.is_empty() {
return Err(Error::validate("'config' section is empty"));
}
Ok(())
}
fn validate_environment(
&self,
environment: &'a cml::Environment,
strong_dependencies: &mut DirectedGraph<DependencyNode<'a>>,
) -> Result<(), Error> {
match &environment.extends {
Some(cml::EnvironmentExtends::None) => {
if environment.stop_timeout_ms.is_none() {
return Err(Error::validate(
"'__stop_timeout_ms' must be provided if the environment does not extend \
another environment",
));
}
}
Some(cml::EnvironmentExtends::Realm) | None => {}
}
if let Some(runners) = &environment.runners {
let mut used_names = HashMap::new();
for registration in runners {
// Validate that this name is not already used.
let name = registration.r#as.as_ref().unwrap_or(&registration.runner);
if let Some(previous_runner) = used_names.insert(name, &registration.runner) {
return Err(Error::validate(format!(
"Duplicate runners registered under name \"{}\": \"{}\" and \"{}\".",
name, &registration.runner, previous_runner
)));
}
// Ensure that the environment is defined in `runners` if it comes from `self`.
if registration.from == cml::RegistrationRef::Self_
&& !self.all_runners.contains(&registration.runner)
{
return Err(Error::validate(format!(
"Runner \"{}\" registered in environment is not in \"runners\"",
&registration.runner,
)));
}
self.validate_component_child_ref(
&format!("\"{}\" runner source", &registration.runner),
&cml::AnyRef::from(&registration.from),
)?;
// Ensure there are no cycles, such as a resolver in an environment being assigned
// to a child which the resolver depends on.
if let Some(source) = DependencyNode::registration_ref(&registration.from) {
let target = DependencyNode::Named(&environment.name);
self.add_strong_dep(None, source, target, strong_dependencies);
}
}
}
if let Some(resolvers) = &environment.resolvers {
let mut used_schemes = HashMap::new();
for registration in resolvers {
// Validate that the scheme is not already used.
if let Some(previous_resolver) =
used_schemes.insert(&registration.scheme, &registration.resolver)
{
return Err(Error::validate(format!(
"scheme \"{}\" for resolver \"{}\" is already registered; \
previously registered to resolver \"{}\".",
&registration.scheme, &registration.resolver, previous_resolver
)));
}
self.validate_component_child_ref(
&format!("\"{}\" resolver source", &registration.resolver),
&cml::AnyRef::from(&registration.from),
)?;
// Ensure there are no cycles, such as a resolver in an environment being assigned
// to a child which the resolver depends on.
if let Some(source) = DependencyNode::registration_ref(&registration.from) {
let target = DependencyNode::Named(&environment.name);
self.add_strong_dep(None, source, target, strong_dependencies);
}
}
}
if let Some(debug_capabilities) = &environment.debug {
for debug in debug_capabilities {
if let Some(protocol) = debug.protocol.as_ref() {
for protocol in protocol.iter() {
if debug.from == cml::OfferFromRef::Self_
&& !self.all_protocols.contains(protocol)
{
return Err(Error::validate(format!(
"Protocol \"{}\" is offered from self, so it must be declared as a \"protocol\" in \"capabilities\"",
protocol
)));
}
}
}
self.validate_from_clause("debug", debug, &None, &None)?;
// Ensure there are no cycles, such as a debug capability in an environment being
// assigned to the child which is providing the capability.
if let Some(source) = DependencyNode::offer_from_ref(&debug.from) {
let target = DependencyNode::Named(&environment.name);
self.add_strong_dep(None, source, target, strong_dependencies);
}
}
}
Ok(())
}
}
/// Given an iterator with `(key, name)` tuples, ensure that `key` doesn't
/// appear twice. `name` is used in generated error messages.
fn ensure_no_duplicate_names<'a, I>(values: I) -> Result<(), Error>
where
I: Iterator<Item = (&'a cml::Name, &'a str)>,
{
let mut seen_keys = HashMap::new();
for (key, name) in values {
if let Some(preexisting_name) = seen_keys.insert(key, name) {
return Err(Error::validate(format!(
"identifier \"{}\" is defined twice, once in \"{}\" and once in \"{}\"",
key, name, preexisting_name
)));
}
}
Ok(())
}
/// Returns an error if the iterator contains duplicate values.
fn ensure_no_duplicate_values<'a, I, V>(values: I) -> Result<(), Error>
where
I: IntoIterator<Item = &'a V>,
V: 'a + Hash + Eq + fmt::Display,
{
let mut seen = HashSet::new();
for value in values {
if !seen.insert(value) {
return Err(Error::validate(format!("Found duplicate value \"{}\" in array.", value)));
}
}
Ok(())
}
/// A node in the DependencyGraph. This enum is used to differentiate between node types.
#[derive(Copy, Clone, Hash, Ord, Debug, PartialOrd, PartialEq, Eq)]
enum DependencyNode<'a> {
Named(&'a cml::Name),
Self_,
}
impl<'a> DependencyNode<'a> {
fn capability_from_ref(ref_: &'a cml::CapabilityFromRef) -> Option<DependencyNode<'a>> {
match ref_ {
cml::CapabilityFromRef::Named(name) => Some(DependencyNode::Named(name)),
cml::CapabilityFromRef::Self_ => Some(DependencyNode::Self_),
// We don't care about cycles with the parent, because those will be resolved when the
// parent manifest is validated.
cml::CapabilityFromRef::Parent => None,
}
}
fn use_from_ref(ref_: Option<&'a cml::UseFromRef>) -> Option<DependencyNode<'a>> {
match ref_ {
Some(cml::UseFromRef::Named(name)) => Some(DependencyNode::Named(name)),
Some(cml::UseFromRef::Self_) => Some(DependencyNode::Self_),
// We don't care about cycles with the parent, because those will be resolved when the
// parent manifest is validated.
Some(cml::UseFromRef::Parent) => None,
// We don't care about cycles with the framework, because the framework always outlives
// a component
Some(cml::UseFromRef::Framework) => None,
// We don't care about cycles with debug, because our environment is controlled by our
// parent
Some(cml::UseFromRef::Debug) => None,
None => None,
}
}
fn offer_from_ref(ref_: &'a cml::OfferFromRef) -> Option<DependencyNode<'a>> {
match ref_ {
cml::OfferFromRef::Named(name) => Some(DependencyNode::Named(name)),
cml::OfferFromRef::Self_ => Some(DependencyNode::Self_),
// We don't care about cycles with the parent, because those will be resolved when the
// parent manifest is validated.
cml::OfferFromRef::Parent => None,
// We don't care about cycles with the framework, because the framework always outlives
// a component
cml::OfferFromRef::Framework => None,
// If the offer source is intentionally omitted, then definitionally this offer does
// not cause an edge in our dependency graph.
cml::OfferFromRef::Void => None,
}
}
fn offer_to_ref(ref_: &'a cml::OfferToRef) -> DependencyNode<'a> {
match ref_ {
cml::OfferToRef::Named(name) => DependencyNode::Named(name),
}
}
fn registration_ref(ref_: &'a cml::RegistrationRef) -> Option<DependencyNode<'a>> {
match ref_ {
cml::RegistrationRef::Named(name) => Some(DependencyNode::Named(name)),
cml::RegistrationRef::Self_ => Some(DependencyNode::Self_),
// We don't care about cycles with the parent, because those will be resolved when the
// parent manifest is validated.
cml::RegistrationRef::Parent => None,
}
}
}
impl<'a> fmt::Display for DependencyNode<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DependencyNode::Self_ => write!(f, "self"),
DependencyNode::Named(name) => write!(f, "#{}", name),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Location;
use assert_matches::assert_matches;
use lazy_static::lazy_static;
use serde_json::json;
use std::io::Write;
use tempfile::TempDir;
macro_rules! test_validate_cml {
(
$(
$test_name:ident($input:expr, $($pattern:tt)+),
)+
) => {
$(
#[test]
fn $test_name() {
let input = format!("{}", $input);
let result = write_and_validate("test.cml", input.as_bytes());
assert_matches!(result, $($pattern)+);
}
)+
}
}
macro_rules! test_validate_cml_with_feature {
(
$features:expr,
{
$(
$test_name:ident($input:expr, $($pattern:tt)+),
)+
}
) => {
$(
#[test]
fn $test_name() {
let input = format!("{}", $input);
let features = $features;
let result = write_and_validate_with_features("test.cml", input.as_bytes(), &features);
assert_matches!(result, $($pattern)+);
}
)+
}
}
macro_rules! test_validate_cmx {
(
$(
$test_name:ident($input:expr, $($pattern:tt)+),
)+
) => {
$(
#[test]
fn $test_name() {
let input = format!("{}", $input);
let result = write_and_validate("test.cmx", input.as_bytes());
assert_matches!(result, $($pattern)+);
}
)+
}
}
fn write_and_validate(filename: &str, input: &[u8]) -> Result<(), Error> {
write_and_validate_with_features(filename, input, &FeatureSet::empty())
}
fn write_and_validate_with_features(
filename: &str,
input: &[u8],
features: &FeatureSet,
) -> Result<(), Error> {
let tmp_dir = TempDir::new().unwrap();
let tmp_file_path = tmp_dir.path().join(filename);
File::create(&tmp_file_path).unwrap().write_all(input).unwrap();
validate(&vec![tmp_file_path], &[], features)
}
#[test]
fn test_validate_invalid_json_fails() {
let result = write_and_validate("test.cml", b"{");
let expected_err = r#" --> 1:2
|
1 | {
| ^---
|
= expected identifier or string"#;
assert_matches!(result, Err(Error::Parse { err, .. }) if &err == expected_err);
}
#[test]
fn test_cml_json5() {
let input = r##"{
"expose": [
// Here are some services to expose.
{ "protocol": "fuchsia.logger.Log", "from": "#logger", },
{ "directory": "blobfs", "from": "#logger", "rights": ["rw*"]},
],
"children": [
{
name: 'logger',
'url': 'fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm',
},
],
}"##;
let result = write_and_validate("test.cml", input.as_bytes());
assert_matches!(result, Ok(()));
}
#[test]
fn test_cml_error_location() {
let input = r##"{
"use": [
{
"event": "started",
"from": "bad",
},
],
}"##;
let result = write_and_validate("test.cml", input.as_bytes());
assert_matches!(
result,
Err(Error::Parse { err, location: Some(l), filename: Some(f) })
if &err == "invalid value: string \"bad\", expected \"parent\", \"framework\", \"debug\", \"self\", \"#<capability-name>\", \"#<child-name>\", or none" &&
l == Location { line: 5, column: 21 } &&
f.ends_with("/test.cml")
);
let input = r##"{
"use": [
{ "event": "started" },
],
}"##;
let result = write_and_validate("test.cml", input.as_bytes());
assert_matches!(
result,
Err(Error::Validate { schema_name: None, err, filename: Some(f) })
if &err == "\"from\" should be present with \"event\"" &&
f.ends_with("/test.cml")
);
}
test_validate_cml! {
// include
test_cml_empty_include(
json!(
{
"include": [],
}
),
Ok(())
),
test_cml_some_include(
json!(
{
"include": [ "some.cml" ],
}
),
Ok(())
),
test_cml_couple_of_include(
json!(
{
"include": [ "some1.cml", "some2.cml" ],
}
),
Ok(())
),
// program
test_cml_empty_json(
json!({}),
Ok(())
),
test_cml_program(
json!(
{
"program": {
"runner": "elf",
"binary": "bin/app",
},
}
),
Ok(())
),
test_cml_program_no_runner(
json!({"program": { "binary": "bin/app" }}),
Err(Error::Validate { schema_name: None, err, .. }) if &err ==
"Component has a `program` block defined, but doesn't specify a `runner`. \
Components need to use a runner to actually execute code."
),
// use
test_cml_use(
json!({
"use": [
{ "protocol": "CoolFonts", "path": "/svc/MyFonts" },
{ "protocol": "CoolFonts2", "path": "/svc/MyFonts2", "from": "debug" },
{ "protocol": "fuchsia.test.hub.HubReport", "from": "framework" },
{ "protocol": "fuchsia.sys2.StorageAdmin", "from": "#data-storage" },
{ "protocol": ["fuchsia.ui.scenic.Scenic", "fuchsia.logger.LogSink"] },
{
"directory": "assets",
"path": "/data/assets",
"rights": ["rw*"],
},
{
"directory": "config",
"from": "parent",
"path": "/data/config",
"rights": ["rx*"],
"subdir": "fonts/all",
},
{ "storage": "data", "path": "/example" },
{ "storage": "cache", "path": "/tmp" },
{ "event": [ "started", "stopped"], "from": "parent" },
{ "event": [ "launched"], "from": "framework" },
{ "event": "destroyed", "from": "framework", "as": "destroyed_x" },
{
"event": "directory_ready_diagnostics",
"as": "directory_ready",
"from": "parent",
"filter": {
"name": "diagnositcs"
}
},
{
"event_stream_deprecated": "my_stream",
"subscriptions": [
{
"event": "started",
},
{
"event": "stopped",
},
{
"event": "launched",
}]
},
{
"event_stream": ["started", "stopped", "running"],
"scope":["#test"],
"path":"/svc/testpath",
"as": "my_cool_stream",
"from":"parent",
},
],
"capabilities": [
{
"storage": "data-storage",
"from": "parent",
"backing_dir": "minfs",
"storage_id": "static_instance_id_or_moniker",
}
]
}),
Ok(())
),
test_cml_use_event_missing_from(
json!({
"use": [
{ "event": "started" },
]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"from\" should be present with \"event\""
),
test_cml_expose_event_stream_multiple_as(
json!({
"expose": [
{
"event_stream": ["started", "stopped"],
"from" : "framework",
"as": "something"
},
]
}),
Err(Error::Validate { err, .. }) if &err == "as cannot be used with multiple events"
),
test_cml_offer_event_stream_multiple_filter(
json!({
"offer": [
{
"event_stream": ["started", "stopped"],
"from" : "framework",
"filter": {"data": "something"},
"to": "#something"
},
]
}),
Err(Error::Validate { err, .. }) if &err == "\"filter\" cannot be used with multiple events."
),
test_cml_offer_event_stream_capability_requested_not_from_framework(
json!({
"offer": [
{
"event_stream": ["capability_requested", "stopped"],
"from" : "parent",
"to": "#something"
},
]
}),
Err(Error::Validate { err, .. }) if &err == "\"#something\" is an \"offer\" target but it does not appear in \"children\" or \"collections\""
),
test_cml_offer_event_stream_capability_requested_no_filter(
json!({
"offer": [
{
"event_stream": ["capability_requested", "stopped"],
"from" : "framework",
"to": "#something"
},
]
}),
Err(Error::Validate { err, .. }) if &err == "\"filter\" must be specified if \"capability_requested\" or \"directory_ready\" are offered from framework"
),
test_cml_offer_event_stream_directory_ready_no_filter(
json!({
"offer": [
{
"event_stream": ["directory_ready", "stopped"],
"from" : "framework",
"to": "#something"
},
]
}),
Err(Error::Validate { err, .. }) if &err == "\"filter\" must be specified if \"capability_requested\" or \"directory_ready\" are offered from framework"
),
test_cml_offer_event_stream_capability_requested_bad_filter(
json!({
"offer": [
{
"event_stream": "stopped",
"from" : "framework",
"to": "#something",
"filter": {
"data": "something"
}
},
]
}),
Err(Error::Validate { err, .. }) if &err == "\"filter\" can only be used when offering \"capability_requested\" or \"directory_ready\" from framework."
),
test_cml_offer_event_stream_capability_requested_with_filter(
json!({
"offer": [
{
"event_stream": "capability_requested",
"from" : "framework",
"to": "#something",
"filter": {"data": "something"}
},
]
}),
Err(Error::Validate { err, .. }) if &err == "\"#something\" is an \"offer\" target but it does not appear in \"children\" or \"collections\""
),
test_cml_offer_event_stream_directory_ready_with_filter(
json!({
"offer": [
{
"event_stream": "directory_ready",
"from" : "framework",
"to": "#something",
"filter": {"data": "something"}
},
]
}),
Err(Error::Validate { err, .. }) if &err == "\"#something\" is an \"offer\" target but it does not appear in \"children\" or \"collections\""
),
test_cml_offer_event_stream_multiple_as(
json!({
"offer": [
{
"event_stream": ["started", "stopped"],
"from" : "framework",
"to": "#self",
"as": "something"
},
]
}),
Err(Error::Validate { err, .. }) if &err == "as cannot be used with multiple events"
),
test_cml_expose_event_stream_from_self(
json!({
"expose": [
{ "event_stream": ["started", "stopped"], "from" : "self" },
]
}),
Err(Error::Validate { err, .. }) if &err == "Cannot expose event_streams from self"
),
test_cml_offer_event_stream_from_self(
json!({
"offer": [
{ "event_stream": ["started", "stopped"], "from" : "self", "to": "#self" },
]
}),
Err(Error::Validate { err, .. }) if &err == "cannot offer an event_stream from self"
),
test_cml_offer_event_stream_from_anything_else(
json!({
"offer": [
{
"event_stream": ["started", "stopped"],
"from" : "framework",
"to": "#self"
},
]
}),
Err(Error::Validate { err, .. }) if &err == "\"#self\" is an \"offer\" target but it does not appear in \"children\" or \"collections\""
),
test_cml_expose_event_stream_to_framework(
json!({
"expose": [
{
"event_stream": ["started", "stopped"],
"from" : "self",
"to": "framework"
},
]
}),
Err(Error::Validate { err, .. }) if &err == "cannot expose an event_stream to framework"
),
test_cml_expose_event_stream_scope_invalid_component(
json!({
"expose": [
{
"event_stream": ["started", "stopped"],
"from" : "framework",
"scope":["#invalid_component"]
},
]
}),
Err(Error::Validate { err, .. }) if &err == "event_stream scope invalid_component did not match a component or collection in this .cml file."
),
test_cml_use_event_stream_duplicate(
json!({
"use": [
{ "event_stream": ["started", "started"], "from" : "parent" },
]
}),
Err(Error::Parse { err, .. }) if &err == "invalid value: array with duplicate element, expected a name or nonempty array of names, with unique elements"
),
test_cml_use_event_stream_overlapping_path(
json!({
"use": [
{ "directory": "foobarbaz", "path": "/foo/bar/baz", "rights": [ "r*" ] },
{
"event_stream": ["started"],
"path": "/foo/bar/baz/er",
"from": "parent",
},
],
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "directory \"/foo/bar/baz\" is a prefix of \"use\" target event_stream \"/foo/bar/baz/er\""
),
test_cml_use_event_stream_no_from(
json!({
"use": [
{
"event_stream": ["started"],
"path": "/foo/bar/baz/er",
},
],
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"from\" should be present with \"event_stream\""
),
test_cml_use_event_stream_invalid_path(
json!({
"use": [
{
"event_stream": ["started"],
"path": "my_stream",
"from": "parent",
},
],
}),
Err(Error::Parse { err, .. }) if &err == "invalid value: string \"my_stream\", expected a path with leading `/` and non-empty segments"
),
test_cml_use_event_self_ref(
json!({
"use": [
{
"event": "started",
"from": "self",
},
]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"from: self\" cannot be used with \"event\""
),
test_cml_use_event_stream_self_ref(
json!({
"use": [
{
"event_stream": ["started"],
"path": "/svc/my_stream",
"from": "self",
},
],
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"from: self\" cannot be used with \"event_stream\""
),
test_cml_use_missing_props(
json!({
"use": [ { "path": "/svc/fuchsia.logger.Log" } ]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "`use` declaration is missing a capability keyword, one of: \"service\", \"protocol\", \"directory\", \"storage\", \"runner\", \"event\", \"event_stream\", \"event_stream_deprecated\""
),
test_cml_use_as_with_protocol(
json!({
"use": [ { "protocol": "foo", "as": "xxx" } ]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"as\" cannot be used with \"protocol\""
),
test_cml_use_invalid_from_with_directory(
json!({
"use": [ { "directory": "foo", "from": "debug" } ]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "only \"protocol\" supports source from \"debug\""
),
test_cml_use_as_with_directory(
json!({
"use": [ { "directory": "foo", "as": "xxx" } ]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"as\" cannot be used with \"directory\""
),
test_cml_use_as_with_storage(
json!({
"use": [ { "storage": "cache", "as": "mystorage" } ]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"as\" cannot be used with \"storage\""
),
test_cml_use_from_with_storage(
json!({
"use": [ { "storage": "cache", "from": "parent" } ]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"from\" cannot be used with \"storage\""
),
test_cml_use_invalid_from(
json!({
"use": [
{ "protocol": "CoolFonts", "from": "bad" }
]
}),
Err(Error::Parse { err, .. }) if &err == "invalid value: string \"bad\", expected \"parent\", \"framework\", \"debug\", \"self\", \"#<capability-name>\", \"#<child-name>\", or none"
),
test_cml_use_from_missing_capability(
json!({
"use": [
{ "protocol": "fuchsia.sys2.Admin", "from": "#mystorage" }
]
}),
Err(Error::Validate { err, .. }) if &err == "\"use\" source \"#mystorage\" does not appear in \"children\" or \"capabilities\""
),
test_cml_use_bad_path(
json!({
"use": [
{
"protocol": ["CoolFonts", "FunkyFonts"],
"path": "/MyFonts"
}
]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"path\" can only be specified when one `protocol` is supplied."
),
test_cml_use_bad_duplicate_target_names(
json!({
"use": [
{ "protocol": "fuchsia.component.Realm" },
{ "protocol": "fuchsia.component.Realm" },
],
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"/svc/fuchsia.component.Realm\" is a duplicate \"use\" target protocol"
),
test_cml_use_empty_protocols(
json!({
"use": [
{
"protocol": [],
},
],
}),
Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected a name or nonempty array of names, with unique elements"
),
test_cml_use_bad_subdir(
json!({
"use": [
{
"directory": "config",
"path": "/config",
"from": "parent",
"rights": [ "r*" ],
"subdir": "/",
},
]
}),
Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/\", expected a path with no leading `/` and non-empty segments"
),
test_cml_use_resolver_fails(
json!({
"use": [
{
"resolver": "pkg_resolver",
},
]
}),
Err(Error::Parse { err, .. }) if &err == "unknown field `resolver`, expected one of `service`, `protocol`, `directory`, `storage`, `event`, `event_stream_deprecated`, `event_stream`, `from`, `path`, `rights`, `subdir`, `as`, `scope`, `filter`, `subscriptions`, `dependency`, `availability`"
),
test_cml_use_disallows_nested_dirs_directory(
json!({
"use": [
{ "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ] },
{ "directory": "foobarbaz", "path": "/foo/bar/baz", "rights": [ "r*" ] },
],
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "directory \"/foo/bar\" is a prefix of \"use\" target directory \"/foo/bar/baz\""
),
test_cml_use_disallows_nested_dirs_storage(
json!({
"use": [
{ "storage": "foobar", "path": "/foo/bar" },
{ "storage": "foobarbaz", "path": "/foo/bar/baz" },
],
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "storage \"/foo/bar\" is a prefix of \"use\" target storage \"/foo/bar/baz\""
),
test_cml_use_disallows_nested_dirs_directory_and_storage(
json!({
"use": [
{ "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ] },
{ "storage": "foobarbaz", "path": "/foo/bar/baz" },
],
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "directory \"/foo/bar\" is a prefix of \"use\" target storage \"/foo/bar/baz\""
),
test_cml_use_disallows_common_prefixes_service(
json!({
"use": [
{ "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ] },
{ "protocol": "fuchsia", "path": "/foo/bar/fuchsia" },
],
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "directory \"/foo/bar\" is a prefix of \"use\" target protocol \"/foo/bar/fuchsia\""
),
test_cml_use_disallows_common_prefixes_protocol(
json!({
"use": [
{ "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ] },
{ "protocol": "fuchsia", "path": "/foo/bar/fuchsia.2" },
],
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "directory \"/foo/bar\" is a prefix of \"use\" target protocol \"/foo/bar/fuchsia.2\""
),
test_cml_use_disallows_filter_on_non_events(
json!({
"use": [
{ "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ], "filter": {"path": "/diagnostics"} },
],
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"filter\" can only be used with \"event\""
),
test_cml_use_bad_as_in_event(
json!({
"use": [
{
"event": ["destroyed", "stopped"],
"from": "parent",
"as": "gone"
}
]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"as\" can only be specified when one `event` is supplied"
),
test_cml_use_invalid_from_in_event(
json!({
"use": [
{
"event": ["destroyed"],
"from": "debug"
}
]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "only \"protocol\" supports source from \"debug\""
),
test_cml_use_duplicate_events(
json!({
"use": [
{
"event": ["destroyed", "started"],
"from": "parent",
},
{
"event": ["destroyed"],
"from": "parent",
}
]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"destroyed\" is a duplicate \"use\" target event"
),
test_cml_use_event_stream_missing_events(
json!({
"use": [
{
"event_stream_deprecated": "stream",
"subscriptions": [
{
"event": "destroyed",
}
],
},
]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "Event \"destroyed\" in event stream not found in any \"use\" declaration."
),
test_cml_use_event_stream_missing_deprecated_events(
json!({
"use": [
{
"event_stream_deprecated": "stream",
"subscriptions": [
{
"event": "destroyed",
}
],
},
]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "Event \"destroyed\" in event stream not found in any \"use\" declaration."
),
test_cml_use_event_stream_duplicate_registration(
json!({
"use": [
{
"event": [ "destroyed" ],
"from": "parent",
},
{
"event_stream_deprecated": "stream",
"subscriptions": [
{
"event": "destroyed",
},
{
"event": "destroyed",
}
],
},
]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "Event \"destroyed\" is duplicated in event stream subscriptions."
),
test_cml_use_event_stream_missing_subscriptions(
json!({
"use": [
{
"event": [ "destroyed" ],
"from": "parent",
},
{
"event_stream_deprecated": "test",
},
]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"event_stream\" must have subscriptions."
),
test_cml_use_event_stream_missing_name(
json!({
"use": [
{
"event": [ "destroyed" ],
"from": "parent",
},
{
"event_stream_deprecated": "test",
},
]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"event_stream\" must have subscriptions."
),
test_cml_use_bad_filter_in_event(
json!({
"use": [
{
"event": ["destroyed", "stopped"],
"filter": {"path": "/diagnostics"},
"from": "parent"
}
]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"filter\" can only be specified when one `event` is supplied"
),
test_cml_use_bad_filter_and_as_in_event(
json!({
"use": [
{
"event": ["destroyed", "stopped"],
"from": "framework",
"as": "gone",
"filter": {"path": "/diagnostics"}
}
]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"as\",\"filter\" can only be specified when one `event` is supplied"
),
test_cml_use_from_child_offer_cycle_strong(
json!({
"capabilities": [
{ "protocol": ["fuchsia.example.Protocol"] },
],
"children": [
{
"name": "child",
"url": "fuchsia-pkg://fuchsia.com/child#meta/child.cm",
},
],
"use": [
{
"protocol": "fuchsia.child.Protocol",
"from": "#child",
},
],
"offer": [
{
"protocol": "fuchsia.example.Protocol",
"from": "self",
"to": [ "#child" ],
},
],
}),
Err(Error::Validate { schema_name: None, err, .. })
if &err == "Strong dependency cycles were found. Break the cycle by removing a \
dependency or marking an offer as weak. Cycles: \
{{#child -> self -> #child}}"
),
test_cml_use_from_parent_weak(
json!({
"use": [
{
"protocol": "fuchsia.parent.Protocol",
"from": "parent",
"dependency": "weak",
},
],
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "Only `use` from children can have dependency: \"weak\""
),
test_cml_use_from_child_offer_cycle_weak(
json!({
"capabilities": [
{ "protocol": ["fuchsia.example.Protocol"] },
],
"children": [
{
"name": "child",
"url": "fuchsia-pkg://fuchsia.com/child#meta/child.cm",
},
],
"use": [
{
"protocol": "fuchsia.example.Protocol",
"from": "#child",
"dependency": "weak",
},
],
"offer": [
{
"protocol": "fuchsia.example.Protocol",
"from": "self",
"to": [ "#child" ],
},
],
}),
Ok(())
),
test_cml_use_from_child_offer_storage_no_cycle(
json!({
"capabilities": [
{
"storage": "cdata",
"from": "#backend",
"backing_dir": "blobfs",
"storage_id": "static_instance_id_or_moniker",
},
{
"storage": "pdata",
"from": "parent",
"backing_dir": "blobfs",
"storage_id": "static_instance_id_or_moniker",
},
],
"children": [
{
"name": "child",
"url": "#meta/child.cm",
},
{
"name": "backend",
"url": "#meta/backend.cm",
},
],
"use": [
{
"protocol": "fuchsia.example.Protocol",
"from": "#child",
},
],
"offer": [
{
"storage": "cdata",
"from": "self",
"to": "#child",
},
{
"storage": "pdata",
"from": "self",
"to": "#child",
},
],
}),
Ok(())
),
test_cml_use_from_child_offer_storage_cycle(
json!({
"capabilities": [
{
"storage": "data",
"from": "self",
"backing_dir": "blobfs",
"storage_id": "static_instance_id_or_moniker",
},
],
"children": [
{
"name": "child",
"url": "#meta/child.cm",
},
],
"use": [
{
"protocol": "fuchsia.example.Protocol",
"from": "#child",
},
],
"offer": [
{
"storage": "data",
"from": "self",
"to": "#child",
},
],
}),
Err(Error::Validate {
schema_name: None,
err,
..
}) if &err ==
"Strong dependency cycles were found. Break the cycle by removing a \
dependency or marking an offer as weak. Cycles: {{#child -> self -> #child}}"
),
// expose
test_cml_expose(
json!({
"expose": [
{
"protocol": "A",
"from": "self",
},
{
"protocol": ["B", "C"],
"from": "self",
},
{
"protocol": "D",
"from": "#mystorage",
},
{
"directory": "blobfs",
"from": "self",
"rights": ["r*"],
"subdir": "blob",
},
{ "directory": "data", "from": "framework" },
{ "runner": "elf", "from": "#logger", },
{ "resolver": "pkg_resolver", "from": "#logger" },
],
"capabilities": [
{ "protocol": ["A", "B", "C"] },
{
"directory": "blobfs",
"path": "/blobfs",
"rights": ["rw*"],
},
{
"storage": "mystorage",
"from": "self",
"backing_dir": "blobfs",
"storage_id": "static_instance_id_or_moniker",
}
],
"children": [
{
"name": "logger",
"url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
},
]
}),
Ok(())
),
test_cml_expose_all_valid_chars(
json!({
"expose": [
{
"protocol": "fuchsia.logger.Log",
"from": "#abcdefghijklmnopqrstuvwxyz0123456789_-.",
},
],
"children": [
{
"name": "abcdefghijklmnopqrstuvwxyz0123456789_-.",
"url": "https://www.google.com/gmail"
},
],
}),
Ok(())
),
test_cml_expose_missing_props(
json!({
"expose": [ {} ]
}),
Err(Error::Parse { err, .. }) if &err == "missing field `from`"
),
test_cml_expose_missing_from(
json!({
"expose": [
{ "protocol": "fuchsia.logger.Log", "from": "#missing" },
],
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"expose\" source \"#missing\" does not appear in \"children\" or \"capabilities\""
),
test_cml_expose_duplicate_target_names(
json!({
"capabilities": [
{ "protocol": "logger" },
],
"expose": [
{ "protocol": "logger", "from": "self", "as": "thing" },
{ "directory": "thing", "from": "#child" , "rights": ["rx*"] },
],
"children": [
{
"name": "child",
"url": "fuchsia-pkg://",
},
],
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"thing\" is a duplicate \"expose\" target capability for \"parent\""
),
test_cml_expose_invalid_multiple_from(
json!({
"capabilities": [
{ "protocol": "fuchsia.logger.Log" },
],
"expose": [
{
"protocol": "fuchsia.logger.Log",
"from": [ "self", "#logger" ],
},
],
"children": [
{
"name": "logger",
"url": "fuchsia-pkg://fuchsia.com/logger#meta/logger.cm",
},
]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"protocol\" capabilities cannot have multiple \"from\" clauses"
),
test_cml_expose_from_missing_named_source(
json!({
"expose": [
{
"protocol": "fuchsia.logger.Log",
"from": "#does-not-exist",
},
],
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"expose\" source \"#does-not-exist\" does not appear in \"children\" or \"capabilities\""
),
test_cml_expose_bad_from(
json!({
"expose": [ {
"protocol": "fuchsia.logger.Log", "from": "parent"
} ]
}),
Err(Error::Parse { err, .. }) if &err == "invalid value: string \"parent\", expected one or an array of \"framework\", \"self\", or \"#<child-name>\""
),
// if "as" is specified, only 1 array item is allowed.
test_cml_expose_bad_as(
json!({
"expose": [
{
"protocol": ["A", "B"],
"from": "#echo_server",
"as": "thing"
},
],
"children": [
{
"name": "echo_server",
"url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm"
}
]
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"as\" can only be specified when one `protocol` is supplied."
),
test_cml_expose_empty_protocols(
json!({
"expose": [
{
"protocol": [],
"from": "#child",
"as": "thing"
},
],
"children": [
{
"name": "child",
"url": "fuchsia-pkg://",
},
],
}),
Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected a name or nonempty array of names, with unique elements"
),
test_cml_expose_bad_subdir(
json!({
"expose": [
{
"directory": "blobfs",
"from": "self",
"rights": ["r*"],
"subdir": "/",
},
]
}),
Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/\", expected a path with no leading `/` and non-empty segments"
),
test_cml_expose_invalid_subdir_to_framework(
json!({
"capabilities": [
{
"directory": "foo",
"rights": ["r*"],
"path": "/foo",
},
],
"expose": [
{
"directory": "foo",
"from": "self",
"to": "framework",
"subdir": "blob",
},
],
"children": [
{
"name": "child",
"url": "fuchsia-pkg://",
},
],
}),
Err(Error::Validate { schema_name: None, err, .. }) if &err == "`subdir` is not supported for expose to framework. Directly expose the subdirectory instead."
),
test_cml_expose_from_self(
json!({
"expose": [
{
"protocol": "foo_protocol",
"from": "self",
},
{
"protocol": [ "bar_protocol", "baz_protocol" ],
"from": "self",
},
{
"directory": "foo_directory",
"from": "self",
},
{
"runner": "foo_runner",
"from": "self",
},
{
"resolver": "foo_resolver",
"from": "self",
},
],
"capabilities": [
{
"protocol": "foo_protocol",
},
{
"protocol": "bar_protocol",
},
{
"protocol": "baz_protocol",