| // 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(®istration.runner); |
| if let Some(previous_runner) = used_names.insert(name, ®istration.runner) { |
| return Err(Error::validate(format!( |
| "Duplicate runners registered under name \"{}\": \"{}\" and \"{}\".", |
| name, ®istration.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(®istration.runner) |
| { |
| return Err(Error::validate(format!( |
| "Runner \"{}\" registered in environment is not in \"runners\"", |
| ®istration.runner, |
| ))); |
| } |
| |
| self.validate_component_child_ref( |
| &format!("\"{}\" runner source", ®istration.runner), |
| &cml::AnyRef::from(®istration.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(®istration.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(®istration.scheme, ®istration.resolver) |
| { |
| return Err(Error::validate(format!( |
| "scheme \"{}\" for resolver \"{}\" is already registered; \ |
| previously registered to resolver \"{}\".", |
| ®istration.scheme, ®istration.resolver, previous_resolver |
| ))); |
| } |
| |
| self.validate_component_child_ref( |
| &format!("\"{}\" resolver source", ®istration.resolver), |
| &cml::AnyRef::from(®istration.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(®istration.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", |
| |