| // 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", |
| }, |
| { |
| "directory": "foo_directory", |
| "path": "/dir", |
| "rights": [ "r*" ], |
| }, |
| { |
| "runner": "foo_runner", |
| "path": "/svc/runner", |
| }, |
| { |
| "resolver": "foo_resolver", |
| "path": "/svc/resolver", |
| }, |
| ] |
| }), |
| Ok(()) |
| ), |
| test_cml_expose_protocol_from_self_missing( |
| json!({ |
| "expose": [ |
| { |
| "protocol": "pkg_protocol", |
| "from": "self", |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Protocol \"pkg_protocol\" is exposed from self, so it must be declared as a \"protocol\" in \"capabilities\"" |
| ), |
| test_cml_expose_protocol_from_self_missing_multiple( |
| json!({ |
| "expose": [ |
| { |
| "protocol": [ "foo_protocol", "bar_protocol" ], |
| "from": "self", |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Protocol \"foo_protocol\" is exposed from self, so it must be declared as a \"protocol\" in \"capabilities\"" |
| ), |
| test_cml_expose_directory_from_self_missing( |
| json!({ |
| "expose": [ |
| { |
| "directory": "pkg_directory", |
| "from": "self", |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Directory \"pkg_directory\" is exposed from self, so it must be declared as a \"directory\" in \"capabilities\"" |
| ), |
| test_cml_expose_runner_from_self_missing( |
| json!({ |
| "expose": [ |
| { |
| "runner": "dart", |
| "from": "self", |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Runner \"dart\" is exposed from self, so it must be declared as a \"runner\" in \"capabilities\"" |
| ), |
| test_cml_expose_resolver_from_self_missing( |
| json!({ |
| "expose": [ |
| { |
| "resolver": "pkg_resolver", |
| "from": "self", |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Resolver \"pkg_resolver\" is exposed from self, so it must be declared as a \"resolver\" in \"capabilities\"" |
| ), |
| test_cml_expose_protocol_from_collection_invalid( |
| json!({ |
| "collections": [ { |
| "name": "coll", |
| "durability": "transient", |
| } ], |
| "expose": [ |
| { "protocol": "fuchsia.logger.Log", "from": "#coll" }, |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"expose\" source \"#coll\" does not appear in \"children\" or \"capabilities\"" |
| ), |
| test_cml_expose_directory_from_collection_invalid( |
| json!({ |
| "collections": [ { |
| "name": "coll", |
| "durability": "transient", |
| } ], |
| "expose": [ |
| { "directory": "temp", "from": "#coll" }, |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"expose\" source \"#coll\" does not appear in \"children\"" |
| ), |
| test_cml_expose_runner_from_collection_invalid( |
| json!({ |
| "collections": [ { |
| "name": "coll", |
| "durability": "transient", |
| } ], |
| "expose": [ |
| { "runner": "elf", "from": "#coll" }, |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"expose\" source \"#coll\" does not appear in \"children\"" |
| ), |
| test_cml_expose_resolver_from_collection_invalid( |
| json!({ |
| "collections": [ { |
| "name": "coll", |
| "durability": "transient", |
| } ], |
| "expose": [ |
| { "resolver": "base", "from": "#coll" }, |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"expose\" source \"#coll\" does not appear in \"children\"" |
| ), |
| test_cml_expose_to_framework_ok( |
| json!({ |
| "capabilities": [ |
| { |
| "directory": "foo", |
| "path": "/foo", |
| "rights": ["r*"], |
| }, |
| ], |
| "expose": [ |
| { |
| "directory": "foo", |
| "from": "self", |
| "to": "framework" |
| } |
| ], |
| "children": [ |
| { |
| "name": "child", |
| "url": "fuchsia-pkg://", |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_expose_to_framework_invalid( |
| json!({ |
| "expose": [ |
| { |
| "directory": "foo", |
| "from": "#logger", |
| "to": "framework" |
| } |
| ], |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm" |
| } |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Expose to framework can only be done from self." |
| ), |
| |
| // offer |
| test_cml_offer( |
| json!({ |
| "offer": [ |
| { |
| "protocol": "fuchsia.fonts.LegacyProvider", |
| "from": "parent", |
| "to": [ "#echo_server" ], |
| "dependency": "weak_for_migration" |
| }, |
| { |
| "protocol": "fuchsia.sys2.StorageAdmin", |
| "from": "#data", |
| "to": [ "#echo_server" ] |
| }, |
| { |
| "protocol": [ |
| "fuchsia.settings.Accessibility", |
| "fuchsia.ui.scenic.Scenic" |
| ], |
| "from": "parent", |
| "to": [ "#echo_server" ], |
| "dependency": "strong" |
| }, |
| { |
| "directory": "assets", |
| "from": "self", |
| "to": [ "#echo_server" ], |
| "rights": ["r*"] |
| }, |
| { |
| "directory": "index", |
| "subdir": "files", |
| "from": "parent", |
| "to": [ "#modular" ], |
| "dependency": "weak_for_migration" |
| }, |
| { |
| "directory": "config", |
| "from": "framework", |
| "to": [ "#modular" ], |
| "as": "config", |
| "dependency": "strong" |
| }, |
| { |
| "storage": "data", |
| "from": "self", |
| "to": [ "#modular", "#logger" ] |
| }, |
| { |
| "runner": "elf", |
| "from": "parent", |
| "to": [ "#modular", "#logger" ] |
| }, |
| { |
| "resolver": "pkg_resolver", |
| "from": "parent", |
| "to": [ "#modular" ], |
| }, |
| { |
| "event": "directory_ready", |
| "from": "parent", |
| "to": [ "#modular" ], |
| "as": "capability-ready-for-modular", |
| "filter": { |
| "name": "modular" |
| } |
| }, |
| ], |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm" |
| }, |
| { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm" |
| }, |
| ], |
| "collections": [ |
| { |
| "name": "modular", |
| "durability": "persistent", |
| }, |
| ], |
| "capabilities": [ |
| { |
| "directory": "assets", |
| "path": "/data/assets", |
| "rights": [ "rw*" ], |
| }, |
| { |
| "storage": "data", |
| "from": "parent", |
| "backing_dir": "minfs", |
| "storage_id": "static_instance_id_or_moniker", |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_offer_all_valid_chars( |
| json!({ |
| "offer": [ |
| { |
| "protocol": "fuchsia.logger.Log", |
| "from": "#abcdefghijklmnopqrstuvwxyz0123456789_-from", |
| "to": [ "#abcdefghijklmnopqrstuvwxyz0123456789_-to" ], |
| }, |
| ], |
| "children": [ |
| { |
| "name": "abcdefghijklmnopqrstuvwxyz0123456789_-from", |
| "url": "https://www.google.com/gmail" |
| }, |
| { |
| "name": "abcdefghijklmnopqrstuvwxyz0123456789_-to", |
| "url": "https://www.google.com/gmail" |
| }, |
| ], |
| "capabilities": [ |
| { |
| "storage": "abcdefghijklmnopqrstuvwxyz0123456789_-storage", |
| "from": "#abcdefghijklmnopqrstuvwxyz0123456789_-from", |
| "backing_dir": "example", |
| "storage_id": "static_instance_id_or_moniker", |
| } |
| ] |
| }), |
| Ok(()) |
| ), |
| test_cml_offer_singleton_to ( |
| json!({ |
| "offer": [ |
| { |
| "protocol": "fuchsia.fonts.LegacyProvider", |
| "from": "parent", |
| "to": "#echo_server", |
| "dependency": "weak_for_migration" |
| }, |
| ], |
| "children": [ |
| { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm" |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_offer_missing_props( |
| json!({ |
| "offer": [ {} ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "missing field `from`" |
| ), |
| test_cml_offer_missing_from( |
| json!({ |
| "offer": [ |
| { |
| "protocol": "fuchsia.logger.Log", |
| "from": "#missing", |
| "to": [ "#echo_server" ], |
| }, |
| ], |
| "children": [ |
| { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo_server#meta/echo_server.cm", |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"offer\" source \"#missing\" does not appear in \"children\" or \"capabilities\"" |
| ), |
| test_cml_storage_offer_from_child( |
| json!({ |
| "offer": [ |
| { |
| "storage": "cache", |
| "from": "#storage_provider", |
| "to": [ "#echo_server" ], |
| }, |
| ], |
| "children": [ |
| { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo_server#meta/echo_server.cm", |
| }, |
| { |
| "name": "storage_provider", |
| "url": "fuchsia-pkg://fuchsia.com/storage_provider#meta/storage_provider.cm", |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Storage \"cache\" is offered from a child, but storage capabilities cannot be exposed" |
| ), |
| test_cml_offer_bad_from( |
| json!({ |
| "offer": [ { |
| "protocol": "fuchsia.logger.Log", |
| "from": "#invalid@", |
| "to": [ "#echo_server" ], |
| } ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"#invalid@\", expected one or an array of \"parent\", \"framework\", \"self\", or \"#<child-name>\"" |
| ), |
| test_cml_offer_invalid_multiple_from( |
| json!({ |
| "offer": [ |
| { |
| "protocol": "fuchsia.logger.Log", |
| "from": [ "parent", "#logger" ], |
| "to": [ "#echo_server" ], |
| }, |
| ], |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger#meta/logger.cm", |
| }, |
| { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm", |
| }, |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"protocol\" capabilities cannot have multiple \"from\" clauses" |
| ), |
| test_cml_offer_from_missing_named_source( |
| json!({ |
| "offer": [ |
| { |
| "protocol": "fuchsia.logger.Log", |
| "from": "#does-not-exist", |
| "to": ["#echo_server" ], |
| }, |
| ], |
| "children": [ |
| { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm", |
| }, |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"offer\" source \"#does-not-exist\" does not appear in \"children\" or \"capabilities\"" |
| ), |
| test_cml_offer_protocol_from_collection_invalid( |
| json!({ |
| "collections": [ { |
| "name": "coll", |
| "durability": "transient", |
| } ], |
| "children": [ { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm", |
| } ], |
| "offer": [ |
| { "protocol": "fuchsia.logger.Log", "from": "#coll", "to": [ "#echo_server" ] }, |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"offer\" source \"#coll\" does not appear in \"children\" or \"capabilities\"" |
| ), |
| test_cml_offer_directory_from_collection_invalid( |
| json!({ |
| "collections": [ { |
| "name": "coll", |
| "durability": "transient", |
| } ], |
| "children": [ { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm", |
| } ], |
| "offer": [ |
| { "directory": "temp", "from": "#coll", "to": [ "#echo_server" ] }, |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"offer\" source \"#coll\" does not appear in \"children\"" |
| ), |
| test_cml_offer_storage_from_collection_invalid( |
| json!({ |
| "collections": [ { |
| "name": "coll", |
| "durability": "transient", |
| } ], |
| "children": [ { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm", |
| } ], |
| "offer": [ |
| { "storage": "cache", "from": "#coll", "to": [ "#echo_server" ] }, |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Storage \"cache\" is offered from a child, but storage capabilities cannot be exposed" |
| ), |
| test_cml_offer_runner_from_collection_invalid( |
| json!({ |
| "collections": [ { |
| "name": "coll", |
| "durability": "transient", |
| } ], |
| "children": [ { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm", |
| } ], |
| "offer": [ |
| { "runner": "elf", "from": "#coll", "to": [ "#echo_server" ] }, |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"offer\" source \"#coll\" does not appear in \"children\"" |
| ), |
| test_cml_offer_resolver_from_collection_invalid( |
| json!({ |
| "collections": [ { |
| "name": "coll", |
| "durability": "transient", |
| } ], |
| "children": [ { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm", |
| } ], |
| "offer": [ |
| { "resolver": "base", "from": "#coll", "to": [ "#echo_server" ] }, |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"offer\" source \"#coll\" does not appear in \"children\"" |
| ), |
| test_cml_offer_event_from_collection_invalid( |
| json!({ |
| "collections": [ { |
| "name": "coll", |
| "durability": "transient", |
| } ], |
| "children": [ { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm", |
| } ], |
| "offer": [ |
| { "event": "started", "from": "#coll", "to": [ "#echo_server" ] }, |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"offer\" source \"#coll\" does not appear in \"children\"" |
| ), |
| test_cml_offer_empty_targets( |
| json!({ |
| "offer": [ |
| { |
| "protocol": "fuchsia.logger.Log", |
| "from": "#child", |
| "to": [] |
| }, |
| ], |
| "children": [ |
| { |
| "name": "child", |
| "url": "fuchsia-pkg://", |
| }, |
| ], |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected one or an array of \"#<child-name>\" or \"#<collection-name>\", with unique elements" |
| ), |
| test_cml_offer_duplicate_targets( |
| json!({ |
| "offer": [ { |
| "protocol": "fuchsia.logger.Log", |
| "from": "#logger", |
| "to": ["#a", "#a"] |
| } ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: array with duplicate element, expected one or an array of \"#<child-name>\" or \"#<collection-name>\", with unique elements" |
| ), |
| test_cml_offer_target_missing_props( |
| json!({ |
| "offer": [ { |
| "protocol": "fuchsia.logger.Log", |
| "from": "#logger", |
| "as": "fuchsia.logger.SysLog", |
| } ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "missing field `to`" |
| ), |
| test_cml_offer_target_missing_to( |
| json!({ |
| "offer": [ { |
| "protocol": "fuchsia.logger.Log", |
| "from": "#logger", |
| "to": [ "#missing" ], |
| } ], |
| "children": [ { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm" |
| } ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"#missing\" is an \"offer\" target but it does not appear in \"children\" or \"collections\"" |
| ), |
| test_cml_offer_target_bad_to( |
| json!({ |
| "offer": [ { |
| "protocol": "fuchsia.logger.Log", |
| "from": "#logger", |
| "to": [ "self" ], |
| "as": "fuchsia.logger.SysLog", |
| } ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"self\", expected \"#<child-name>\" or \"#<collection-name>\"" |
| ), |
| test_cml_offer_empty_protocols( |
| json!({ |
| "offer": [ |
| { |
| "protocol": [], |
| "from": "parent", |
| "to": [ "#echo_server" ], |
| "as": "thing" |
| }, |
| ], |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected a name or nonempty array of names, with unique elements" |
| ), |
| test_cml_offer_target_equals_from( |
| json!({ |
| "offer": [ { |
| "protocol": "fuchsia.logger.Log", |
| "from": "#logger", |
| "to": [ "#logger" ], |
| "as": "fuchsia.logger.SysLog", |
| } ], |
| "children": [ { |
| "name": "logger", "url": "fuchsia-pkg://fuchsia.com/logger#meta/logger.cm", |
| } ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Offer target \"#logger\" is same as source" |
| ), |
| test_cml_storage_offer_target_equals_from( |
| json!({ |
| "offer": [ { |
| "storage": "minfs", |
| "from": "self", |
| "to": [ "#logger" ], |
| } ], |
| "children": [ { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger#meta/logger.cm", |
| } ], |
| "capabilities": [ { |
| "storage": "minfs", |
| "from": "#logger", |
| "backing_dir": "minfs-dir", |
| "storage_id": "static_instance_id_or_moniker", |
| } ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Storage offer target \"#logger\" is same as source" |
| ), |
| test_cml_offer_duplicate_target_names( |
| json!({ |
| "offer": [ |
| { |
| "protocol": "logger", |
| "from": "parent", |
| "to": [ "#echo_server" ], |
| "as": "thing" |
| }, |
| { |
| "protocol": "logger", |
| "from": "parent", |
| "to": [ "#scenic" ], |
| }, |
| { |
| "directory": "thing", |
| "from": "parent", |
| "to": [ "#echo_server" ], |
| } |
| ], |
| "children": [ |
| { |
| "name": "scenic", |
| "url": "fuchsia-pkg://fuchsia.com/scenic/stable#meta/scenic.cm" |
| }, |
| { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm" |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"thing\" is a duplicate \"offer\" target capability for \"#echo_server\"" |
| ), |
| test_cml_offer_duplicate_storage_names( |
| json!({ |
| "offer": [ |
| { |
| "storage": "cache", |
| "from": "parent", |
| "to": [ "#echo_server" ] |
| }, |
| { |
| "storage": "cache", |
| "from": "self", |
| "to": [ "#echo_server" ] |
| } |
| ], |
| "capabilities": [ { |
| "storage": "cache", |
| "from": "self", |
| "backing_dir": "minfs", |
| "storage_id": "static_instance_id_or_moniker", |
| } ], |
| "children": [ { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm" |
| } ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"cache\" is a duplicate \"offer\" target capability for \"#echo_server\"" |
| ), |
| // if "as" is specified, only 1 array item is allowed. |
| test_cml_offer_bad_as( |
| json!({ |
| "offer": [ |
| { |
| "protocol": ["A", "B"], |
| "from": "parent", |
| "to": [ "#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_offer_bad_subdir( |
| json!({ |
| "offer": [ |
| { |
| "directory": "index", |
| "subdir": "/", |
| "from": "parent", |
| "to": [ "#modular" ], |
| }, |
| ], |
| "children": [ |
| { |
| "name": "modular", |
| "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm" |
| } |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/\", expected a path with no leading `/` and non-empty segments" |
| ), |
| test_cml_offer_from_self( |
| json!({ |
| "offer": [ |
| { |
| "protocol": "foo_protocol", |
| "from": "self", |
| "to": [ "#modular" ], |
| }, |
| { |
| "protocol": [ "bar_protocol", "baz_protocol" ], |
| "from": "self", |
| "to": [ "#modular" ], |
| }, |
| { |
| "directory": "foo_directory", |
| "from": "self", |
| "to": [ "#modular" ], |
| }, |
| { |
| "runner": "foo_runner", |
| "from": "self", |
| "to": [ "#modular" ], |
| }, |
| { |
| "resolver": "foo_resolver", |
| "from": "self", |
| "to": [ "#modular" ], |
| }, |
| ], |
| "children": [ |
| { |
| "name": "modular", |
| "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm" |
| }, |
| ], |
| "capabilities": [ |
| { |
| "protocol": "foo_protocol", |
| }, |
| { |
| "protocol": "bar_protocol", |
| }, |
| { |
| "protocol": "baz_protocol", |
| }, |
| { |
| "directory": "foo_directory", |
| "path": "/dir", |
| "rights": [ "r*" ], |
| }, |
| { |
| "runner": "foo_runner", |
| "path": "/svc/fuchsia.sys2.ComponentRunner", |
| }, |
| { |
| "resolver": "foo_resolver", |
| "path": "/svc/fuchsia.component.resolution.Resolver", |
| }, |
| ] |
| }), |
| Ok(()) |
| ), |
| test_cml_offer_protocol_from_self_missing( |
| json!({ |
| "offer": [ |
| { |
| "protocol": "pkg_protocol", |
| "from": "self", |
| "to": [ "#modular" ], |
| }, |
| ], |
| "children": [ |
| { |
| "name": "modular", |
| "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm" |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Protocol \"pkg_protocol\" is offered from self, so it must be declared as a \"protocol\" in \"capabilities\"" |
| ), |
| test_cml_offer_protocol_from_self_missing_multiple( |
| json!({ |
| "offer": [ |
| { |
| "protocol": [ "foo_protocol", "bar_protocol" ], |
| "from": "self", |
| "to": [ "#modular" ], |
| }, |
| ], |
| "children": [ |
| { |
| "name": "modular", |
| "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm" |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Protocol \"foo_protocol\" is offered from self, so it must be declared as a \"protocol\" in \"capabilities\"" |
| ), |
| test_cml_offer_directory_from_self_missing( |
| json!({ |
| "offer": [ |
| { |
| "directory": "pkg_directory", |
| "from": "self", |
| "to": [ "#modular" ], |
| }, |
| ], |
| "children": [ |
| { |
| "name": "modular", |
| "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm" |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Directory \"pkg_directory\" is offered from self, so it must be declared as a \"directory\" in \"capabilities\"" |
| ), |
| test_cml_offer_runner_from_self_missing( |
| json!({ |
| "offer": [ |
| { |
| "runner": "dart", |
| "from": "self", |
| "to": [ "#modular" ], |
| }, |
| ], |
| "children": [ |
| { |
| "name": "modular", |
| "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm" |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Runner \"dart\" is offered from self, so it must be declared as a \"runner\" in \"capabilities\"" |
| ), |
| test_cml_offer_resolver_from_self_missing( |
| json!({ |
| "offer": [ |
| { |
| "resolver": "pkg_resolver", |
| "from": "self", |
| "to": [ "#modular" ], |
| }, |
| ], |
| "children": [ |
| { |
| "name": "modular", |
| "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm" |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Resolver \"pkg_resolver\" is offered from self, so it must be declared as a \"resolver\" in \"capabilities\"" |
| ), |
| test_cml_offer_storage_from_self_missing( |
| json!({ |
| "offer": [ |
| { |
| "storage": "cache", |
| "from": "self", |
| "to": [ "#echo_server" ], |
| }, |
| ], |
| "children": [ |
| { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo_server#meta/echo_server.cm", |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Storage \"cache\" is offered from self, so it must be declared as a \"storage\" in \"capabilities\"" |
| ), |
| test_cml_offer_dependency_on_wrong_type( |
| json!({ |
| "offer": [ { |
| "resolver": "fuchsia.logger.Log", |
| "from": "parent", |
| "to": [ "#echo_server" ], |
| "dependency": "strong", |
| } ], |
| "children": [ { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm", |
| } ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Dependency can only be provided for protocol and directory capabilities" |
| ), |
| test_cml_offer_dependency_cycle( |
| json!({ |
| "offer": [ |
| { |
| "protocol": "fuchsia.logger.Log", |
| "from": "#a", |
| "to": [ "#b" ], |
| "dependency": "strong" |
| }, |
| { |
| "directory": "data", |
| "from": "#b", |
| "to": [ "#c" ], |
| }, |
| { |
| "protocol": "ethernet", |
| "from": "#c", |
| "to": [ "#a" ], |
| }, |
| { |
| "runner": "elf", |
| "from": "#b", |
| "to": [ "#d" ], |
| }, |
| { |
| "resolver": "http", |
| "from": "#d", |
| "to": [ "#b" ], |
| }, |
| ], |
| "children": [ |
| { |
| "name": "a", |
| "url": "fuchsia-pkg://fuchsia.com/a#meta/a.cm" |
| }, |
| { |
| "name": "b", |
| "url": "fuchsia-pkg://fuchsia.com/b#meta/b.cm" |
| }, |
| { |
| "name": "c", |
| "url": "fuchsia-pkg://fuchsia.com/b#meta/c.cm" |
| }, |
| { |
| "name": "d", |
| "url": "fuchsia-pkg://fuchsia.com/b#meta/d.cm" |
| }, |
| ] |
| }), |
| 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: \ |
| {{#a -> #b -> #c -> #a}, {#b -> #d -> #b}}" |
| ), |
| test_cml_offer_dependency_cycle_storage( |
| json!({ |
| "capabilities": [ |
| { |
| "storage": "data", |
| "from": "#backend", |
| "backing_dir": "blobfs", |
| "storage_id": "static_instance_id_or_moniker", |
| }, |
| ], |
| "children": [ |
| { |
| "name": "child", |
| "url": "#meta/child.cm", |
| }, |
| { |
| "name": "backend", |
| "url": "#meta/backend.cm", |
| }, |
| ], |
| "offer": [ |
| { |
| "protocol": "fuchsia.example.Protocol", |
| "from": "#child", |
| "to": "#backend", |
| }, |
| { |
| "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: \ |
| {{#backend -> #child -> #backend}}" |
| ), |
| test_cml_offer_weak_dependency_cycle( |
| json!({ |
| "offer": [ |
| { |
| "protocol": "fuchsia.logger.Log", |
| "from": "#child_a", |
| "to": [ "#child_b" ], |
| "dependency": "weak_for_migration" |
| }, |
| { |
| "directory": "data", |
| "from": "#child_b", |
| "to": [ "#child_a" ], |
| }, |
| ], |
| "children": [ |
| { |
| "name": "child_a", |
| "url": "fuchsia-pkg://fuchsia.com/child_a#meta/child_a.cm" |
| }, |
| { |
| "name": "child_b", |
| "url": "fuchsia-pkg://fuchsia.com/child_b#meta/child_b.cm" |
| }, |
| ] |
| }), |
| Ok(()) |
| ), |
| test_cml_offer_disallows_filter_on_non_events( |
| json!({ |
| "offer": [ |
| { |
| "directory": "mydir", |
| "rights": [ "r*" ], |
| "from": "parent", |
| "to": [ "#logger" ], |
| "filter": {"path": "/diagnostics"} |
| }, |
| ], |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm" |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"filter\" can only be used with \"event\" or \"event_stream\"" |
| ), |
| |
| // children |
| test_cml_children( |
| json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| "on_terminate": "reboot", |
| }, |
| { |
| "name": "gmail", |
| "url": "https://www.google.com/gmail", |
| "startup": "eager", |
| }, |
| ] |
| }), |
| Ok(()) |
| ), |
| test_cml_children_missing_props( |
| json!({ |
| "children": [ {} ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "missing field `name`" |
| ), |
| test_cml_children_duplicate_names( |
| json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm" |
| }, |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/beta#meta/logger.cm" |
| } |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "identifier \"logger\" is defined twice, once in \"children\" and once in \"children\"" |
| ), |
| test_cml_children_bad_startup( |
| json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| "startup": "zzz", |
| }, |
| ], |
| }), |
| Err(Error::Parse { err, .. }) if &err == "unknown variant `zzz`, expected `lazy` or `eager`" |
| ), |
| test_cml_children_bad_on_terminate( |
| json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| "on_terminate": "zzz", |
| }, |
| ], |
| }), |
| Err(Error::Parse { err, .. }) if &err == "unknown variant `zzz`, expected `none` or `reboot`" |
| ), |
| |
| |
| test_cml_children_bad_environment( |
| json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| "environment": "parent", |
| } |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"parent\", expected \"#<environment-name>\"" |
| ), |
| test_cml_children_unknown_environment( |
| json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| "environment": "#foo_env", |
| } |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"foo_env\" does not appear in \"environments\"" |
| ), |
| test_cml_children_environment( |
| json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| "environment": "#foo_env", |
| } |
| ], |
| "environments": [ |
| { |
| "name": "foo_env", |
| } |
| ] |
| }), |
| Ok(()) |
| ), |
| test_cml_collections_bad_environment( |
| json!({ |
| "collections": [ |
| { |
| "name": "tests", |
| "durability": "transient", |
| "environment": "parent", |
| } |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"parent\", expected \"#<environment-name>\"" |
| ), |
| test_cml_collections_unknown_environment( |
| json!({ |
| "collections": [ |
| { |
| "name": "tests", |
| "durability": "transient", |
| "environment": "#foo_env", |
| } |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"foo_env\" does not appear in \"environments\"" |
| ), |
| test_cml_collections_environment( |
| json!({ |
| "collections": [ |
| { |
| "name": "tests", |
| "durability": "transient", |
| "environment": "#foo_env", |
| } |
| ], |
| "environments": [ |
| { |
| "name": "foo_env", |
| } |
| ] |
| }), |
| Ok(()) |
| ), |
| |
| test_cml_environment_timeout( |
| json!({ |
| "environments": [ |
| { |
| "name": "foo_env", |
| "__stop_timeout_ms": 10000, |
| } |
| ] |
| }), |
| Ok(()) |
| ), |
| |
| test_cml_environment_bad_timeout( |
| json!({ |
| "environments": [ |
| { |
| "name": "foo_env", |
| "__stop_timeout_ms": -3, |
| } |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: integer `-3`, expected an unsigned 32-bit integer" |
| ), |
| test_cml_environment_debug( |
| json!({ |
| "capabilities": [ |
| { |
| "protocol": "fuchsia.logger.Log2", |
| }, |
| ], |
| "environments": [ |
| { |
| "name": "foo_env", |
| "extends": "realm", |
| "debug": [ |
| { |
| "protocol": "fuchsia.module.Module", |
| "from": "#modular", |
| }, |
| { |
| "protocol": "fuchsia.logger.OtherLog", |
| "from": "parent", |
| }, |
| { |
| "protocol": "fuchsia.logger.Log2", |
| "from": "self", |
| }, |
| ] |
| } |
| ], |
| "children": [ |
| { |
| "name": "modular", |
| "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm" |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_environment_debug_missing_capability( |
| json!({ |
| "environments": [ |
| { |
| "name": "foo_env", |
| "extends": "realm", |
| "debug": [ |
| { |
| "protocol": "fuchsia.module.Module", |
| "from": "#modular", |
| }, |
| { |
| "protocol": "fuchsia.logger.OtherLog", |
| "from": "parent", |
| }, |
| { |
| "protocol": "fuchsia.logger.Log2", |
| "from": "self", |
| }, |
| ] |
| } |
| ], |
| "children": [ |
| { |
| "name": "modular", |
| "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm" |
| }, |
| ], |
| }), |
| Err(Error::Validate { err, .. }) if &err == "Protocol \"fuchsia.logger.Log2\" is offered from self, so it must be declared as a \"protocol\" in \"capabilities\"" |
| ), |
| test_cml_environment_invalid_from_child( |
| json!({ |
| "capabilities": [ |
| { |
| "protocol": "fuchsia.logger.Log2", |
| }, |
| ], |
| "environments": [ |
| { |
| "name": "foo_env", |
| "extends": "realm", |
| "debug": [ |
| { |
| "protocol": "fuchsia.module.Module", |
| "from": "#missing", |
| }, |
| { |
| "protocol": "fuchsia.logger.OtherLog", |
| "from": "parent", |
| }, |
| { |
| "protocol": "fuchsia.logger.Log2", |
| "from": "self", |
| }, |
| ] |
| } |
| ], |
| "children": [ |
| { |
| "name": "modular", |
| "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm" |
| }, |
| ], |
| }), |
| Err(Error::Validate { err, .. }) if &err == "\"debug\" source \"#missing\" does not appear in \"children\" or \"capabilities\"" |
| ), |
| |
| |
| // collections |
| test_cml_collections( |
| json!({ |
| "collections": [ |
| { |
| "name": "modular", |
| "durability": "persistent" |
| }, |
| { |
| "name": "tests", |
| "durability": "transient" |
| }, |
| ] |
| }), |
| Ok(()) |
| ), |
| test_cml_collections_missing_props( |
| json!({ |
| "collections": [ {} ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "missing field `name`" |
| ), |
| test_cml_collections_duplicate_names( |
| json!({ |
| "collections": [ |
| { |
| "name": "modular", |
| "durability": "persistent" |
| }, |
| { |
| "name": "modular", |
| "durability": "transient" |
| } |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "identifier \"modular\" is defined twice, once in \"collections\" and once in \"collections\"" |
| ), |
| test_cml_collections_bad_durability( |
| json!({ |
| "collections": [ |
| { |
| "name": "modular", |
| "durability": "zzz", |
| }, |
| ], |
| }), |
| Err(Error::Parse { err, .. }) if &err == "unknown variant `zzz`, expected one of `persistent`, `transient`, `single_run`" |
| ), |
| |
| // capabilities |
| test_cml_protocol( |
| json!({ |
| "capabilities": [ |
| { |
| "protocol": "a", |
| "path": "/minfs", |
| }, |
| { |
| "protocol": "b", |
| "path": "/data", |
| }, |
| { |
| "protocol": "c", |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_protocol_multi( |
| json!({ |
| "capabilities": [ |
| { |
| "protocol": ["a", "b", "c"], |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_protocol_multi_invalid_path( |
| json!({ |
| "capabilities": [ |
| { |
| "protocol": ["a", "b", "c"], |
| "path": "/minfs", |
| }, |
| ], |
| }), |
| Err(Error::Validate { err, .. }) if &err == "\"path\" can only be specified when one `protocol` is supplied." |
| ), |
| test_cml_protocol_all_valid_chars( |
| json!({ |
| "capabilities": [ |
| { |
| "protocol": "abcdefghijklmnopqrstuvwxyz0123456789_-service", |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_directory( |
| json!({ |
| "capabilities": [ |
| { |
| "directory": "a", |
| "path": "/minfs", |
| "rights": ["connect"], |
| }, |
| { |
| "directory": "b", |
| "path": "/data", |
| "rights": ["connect"], |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_directory_all_valid_chars( |
| json!({ |
| "capabilities": [ |
| { |
| "directory": "abcdefghijklmnopqrstuvwxyz0123456789_-service", |
| "path": "/data", |
| "rights": ["connect"], |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_directory_missing_path( |
| json!({ |
| "capabilities": [ |
| { |
| "directory": "dir", |
| "rights": ["connect"], |
| }, |
| ] |
| }), |
| Err(Error::Validate { err, .. }) if &err == "\"path\" should be present with \"directory\"" |
| ), |
| test_cml_directory_missing_rights( |
| json!({ |
| "capabilities": [ |
| { |
| "directory": "dir", |
| "path": "/dir", |
| }, |
| ] |
| }), |
| Err(Error::Validate { err, .. }) if &err == "\"rights\" should be present with \"directory\"" |
| ), |
| test_cml_storage( |
| json!({ |
| "capabilities": [ |
| { |
| "storage": "a", |
| "from": "#minfs", |
| "backing_dir": "minfs", |
| "storage_id": "static_instance_id", |
| }, |
| { |
| "storage": "b", |
| "from": "parent", |
| "backing_dir": "data", |
| "storage_id": "static_instance_id_or_moniker", |
| }, |
| { |
| "storage": "c", |
| "from": "self", |
| "backing_dir": "storage", |
| "storage_id": "static_instance_id_or_moniker", |
| }, |
| ], |
| "children": [ |
| { |
| "name": "minfs", |
| "url": "fuchsia-pkg://fuchsia.com/minfs/stable#meta/minfs.cm", |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_storage_all_valid_chars( |
| json!({ |
| "capabilities": [ |
| { |
| "storage": "abcdefghijklmnopqrstuvwxyz0123456789_-storage", |
| "from": "#abcdefghijklmnopqrstuvwxyz0123456789_-from", |
| "backing_dir": "example", |
| "storage_id": "static_instance_id_or_moniker", |
| }, |
| ], |
| "children": [ |
| { |
| "name": "abcdefghijklmnopqrstuvwxyz0123456789_-from", |
| "url": "https://www.google.com/gmail", |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_storage_invalid_from( |
| json!({ |
| "capabilities": [ { |
| "storage": "minfs", |
| "from": "#missing", |
| "backing_dir": "minfs", |
| "storage_id": "static_instance_id_or_moniker", |
| } ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"capabilities\" source \"#missing\" does not appear in \"children\"" |
| ), |
| test_cml_storage_missing_path_or_backing_dir( |
| json!({ |
| "capabilities": [ { |
| "storage": "minfs", |
| "from": "self", |
| "storage_id": "static_instance_id_or_moniker", |
| } ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"backing_dir\" should be present with \"storage\"" |
| |
| ), |
| test_cml_storage_missing_storage_id( |
| json!({ |
| "capabilities": [ { |
| "storage": "minfs", |
| "from": "self", |
| "backing_dir": "storage", |
| }, ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"storage_id\" should be present with \"storage\"" |
| ), |
| test_cml_storage_path( |
| json!({ |
| "capabilities": [ { |
| "storage": "minfs", |
| "from": "self", |
| "path": "/minfs", |
| "storage_id": "static_instance_id_or_moniker", |
| } ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"path\" can not be present with \"storage\", use \"backing_dir\"" |
| ), |
| test_cml_runner( |
| json!({ |
| "capabilities": [ |
| { |
| "runner": "a", |
| "path": "/minfs", |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_runner_all_valid_chars( |
| json!({ |
| "children": [ |
| { |
| "name": "abcdefghijklmnopqrstuvwxyz0123456789_-from", |
| "url": "https://www.google.com/gmail" |
| }, |
| ], |
| "capabilities": [ |
| { |
| "runner": "abcdefghijklmnopqrstuvwxyz0123456789_-runner", |
| "path": "/example", |
| }, |
| ] |
| }), |
| Ok(()) |
| ), |
| test_cml_runner_extraneous_from( |
| json!({ |
| "capabilities": [ |
| { |
| "runner": "a", |
| "path": "/example", |
| "from": "self", |
| }, |
| ] |
| }), |
| Err(Error::Validate { err, .. }) if &err == "\"from\" should not be present with \"runner\"" |
| ), |
| test_cml_capability_missing_name( |
| json!({ |
| "capabilities": [ |
| { |
| "path": "/svc/fuchsia.component.resolution.Resolver", |
| }, |
| ] |
| }), |
| Err(Error::Validate { err, .. }) if &err == "`capability` declaration is missing a capability keyword, one of: \"service\", \"protocol\", \"directory\", \"storage\", \"runner\", \"resolver\", \"event\", \"event_stream\"" |
| ), |
| test_cml_resolver_missing_path( |
| json!({ |
| "capabilities": [ |
| { |
| "resolver": "pkg_resolver", |
| }, |
| ] |
| }), |
| Err(Error::Validate { err, .. }) if &err == "\"path\" should be present with \"resolver\"" |
| ), |
| test_cml_capabilities_extraneous_from( |
| json!({ |
| "capabilities": [ |
| { |
| "resolver": "pkg_resolver", |
| "path": "/svc/fuchsia.component.resolution.Resolver", |
| "from": "self", |
| }, |
| ] |
| }), |
| Err(Error::Validate { err, .. }) if &err == "\"from\" should not be present with \"resolver\"" |
| ), |
| test_cml_capabilities_duplicates( |
| json!({ |
| "capabilities": [ |
| { |
| "runner": "pkg_resolver", |
| "path": "/svc/fuchsia.component.resolution.Resolver", |
| }, |
| { |
| "resolver": "pkg_resolver", |
| "path": "/svc/my-resolver", |
| }, |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "identifier \"pkg_resolver\" is defined twice, once in \"resolvers\" and once in \"runners\"" |
| ), |
| |
| // environments |
| test_cml_environments( |
| json!({ |
| "environments": [ |
| { |
| "name": "my_env_a", |
| }, |
| { |
| "name": "my_env_b", |
| "extends": "realm", |
| }, |
| { |
| "name": "my_env_c", |
| "extends": "none", |
| "__stop_timeout_ms": 8000, |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| |
| test_invalid_cml_environment_no_stop_timeout( |
| json!({ |
| "environments": [ |
| { |
| "name": "my_env", |
| "extends": "none", |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == |
| "'__stop_timeout_ms' must be provided if the environment does not extend \ |
| another environment" |
| ), |
| |
| test_cml_environment_invalid_extends( |
| json!({ |
| "environments": [ |
| { |
| "name": "my_env", |
| "extends": "some_made_up_string", |
| }, |
| ], |
| }), |
| Err(Error::Parse { err, .. }) if &err == "unknown variant `some_made_up_string`, expected `realm` or `none`" |
| ), |
| test_cml_environment_missing_props( |
| json!({ |
| "environments": [ {} ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "missing field `name`" |
| ), |
| |
| test_cml_environment_with_runners( |
| json!({ |
| "environments": [ |
| { |
| "name": "my_env", |
| "extends": "realm", |
| "runners": [ |
| { |
| "runner": "dart", |
| "from": "parent", |
| } |
| ] |
| } |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_environment_with_runners_alias( |
| json!({ |
| "environments": [ |
| { |
| "name": "my_env", |
| "extends": "realm", |
| "runners": [ |
| { |
| "runner": "dart", |
| "from": "parent", |
| "as": "my-dart", |
| } |
| ] |
| } |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_environment_with_runners_missing( |
| json!({ |
| "environments": [ |
| { |
| "name": "my_env", |
| "extends": "realm", |
| "runners": [ |
| { |
| "runner": "dart", |
| "from": "self", |
| } |
| ] |
| } |
| ], |
| "capabilities": [ |
| { |
| "runner": "dart", |
| "path": "/svc/fuchsia.component.Runner", |
| } |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_environment_with_runners_bad_name( |
| json!({ |
| "environments": [ |
| { |
| "name": "my_env", |
| "extends": "realm", |
| "runners": [ |
| { |
| "runner": "elf", |
| "from": "parent", |
| "as": "#elf", |
| } |
| ] |
| } |
| ], |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"#elf\", expected a \ |
| name that consists of [A-Za-z0-9_.-] and starts with [A-Za-z0-9_]" |
| ), |
| test_cml_environment_with_runners_duplicate_name( |
| json!({ |
| "environments": [ |
| { |
| "name": "my_env", |
| "extends": "realm", |
| "runners": [ |
| { |
| "runner": "dart", |
| "from": "parent", |
| }, |
| { |
| "runner": "other-dart", |
| "from": "parent", |
| "as": "dart", |
| } |
| ] |
| } |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Duplicate runners registered under name \"dart\": \"other-dart\" and \"dart\"." |
| ), |
| test_cml_environment_with_runner_from_missing_child( |
| json!({ |
| "environments": [ |
| { |
| "name": "my_env", |
| "extends": "realm", |
| "runners": [ |
| { |
| "runner": "elf", |
| "from": "#missing_child", |
| } |
| ] |
| } |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"elf\" runner source \"#missing_child\" does not appear in \"children\"" |
| ), |
| test_cml_environment_with_runner_cycle( |
| json!({ |
| "environments": [ |
| { |
| "name": "my_env", |
| "extends": "realm", |
| "runners": [ |
| { |
| "runner": "elf", |
| "from": "#child", |
| "as": "my-elf", |
| } |
| ] |
| } |
| ], |
| "children": [ |
| { |
| "name": "child", |
| "url": "fuchsia-pkg://child", |
| "environment": "#my_env", |
| } |
| ] |
| }), |
| Err(Error::Validate { err, schema_name: None, .. }) if &err == |
| "Strong dependency cycles were found. Break the cycle by removing a \ |
| dependency or marking an offer as weak. Cycles: \ |
| {{#child -> #my_env -> #child}}" |
| ), |
| test_cml_environment_with_resolvers( |
| json!({ |
| "environments": [ |
| { |
| "name": "my_env", |
| "extends": "realm", |
| "resolvers": [ |
| { |
| "resolver": "pkg_resolver", |
| "from": "parent", |
| "scheme": "fuchsia-pkg", |
| } |
| ] |
| } |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_environment_with_resolvers_bad_scheme( |
| json!({ |
| "environments": [ |
| { |
| "name": "my_env", |
| "extends": "realm", |
| "resolvers": [ |
| { |
| "resolver": "pkg_resolver", |
| "from": "parent", |
| "scheme": "9scheme", |
| } |
| ] |
| } |
| ], |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"9scheme\", expected a valid URL scheme" |
| ), |
| test_cml_environment_with_resolvers_duplicate_scheme( |
| json!({ |
| "environments": [ |
| { |
| "name": "my_env", |
| "extends": "realm", |
| "resolvers": [ |
| { |
| "resolver": "pkg_resolver", |
| "from": "parent", |
| "scheme": "fuchsia-pkg", |
| }, |
| { |
| "resolver": "base_resolver", |
| "from": "parent", |
| "scheme": "fuchsia-pkg", |
| } |
| ] |
| } |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "scheme \"fuchsia-pkg\" for resolver \"base_resolver\" is already registered; previously registered to resolver \"pkg_resolver\"." |
| ), |
| test_cml_environment_with_resolver_from_missing_child( |
| json!({ |
| "environments": [ |
| { |
| "name": "my_env", |
| "extends": "realm", |
| "resolvers": [ |
| { |
| "resolver": "pkg_resolver", |
| "from": "#missing_child", |
| "scheme": "fuchsia-pkg", |
| } |
| ] |
| } |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"pkg_resolver\" resolver source \"#missing_child\" does not appear in \"children\"" |
| ), |
| test_cml_environment_with_resolver_cycle( |
| json!({ |
| "environments": [ |
| { |
| "name": "my_env", |
| "extends": "realm", |
| "resolvers": [ |
| { |
| "resolver": "pkg_resolver", |
| "from": "#child", |
| "scheme": "fuchsia-pkg", |
| } |
| ] |
| } |
| ], |
| "children": [ |
| { |
| "name": "child", |
| "url": "fuchsia-pkg://child", |
| "environment": "#my_env", |
| } |
| ] |
| }), |
| 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 -> #my_env -> #child}}" |
| ), |
| test_cml_environment_with_cycle_multiple_components( |
| json!({ |
| "environments": [ |
| { |
| "name": "my_env", |
| "extends": "realm", |
| "resolvers": [ |
| { |
| "resolver": "pkg_resolver", |
| "from": "#b", |
| "scheme": "fuchsia-pkg", |
| } |
| ] |
| } |
| ], |
| "children": [ |
| { |
| "name": "a", |
| "url": "fuchsia-pkg://a", |
| "environment": "#my_env", |
| }, |
| { |
| "name": "b", |
| "url": "fuchsia-pkg://b", |
| } |
| ], |
| "offer": [ |
| { |
| "protocol": "fuchsia.logger.Log", |
| "from": "#a", |
| "to": [ "#b" ], |
| "dependency": "strong" |
| }, |
| ] |
| }), |
| 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: {{#a -> #b -> #my_env -> #a}}" |
| ), |
| |
| // facets |
| test_cml_facets( |
| json!({ |
| "facets": { |
| "metadata": { |
| "title": "foo", |
| "authors": [ "me", "you" ], |
| "year": 2018 |
| } |
| } |
| }), |
| Ok(()) |
| ), |
| test_cml_facets_wrong_type( |
| json!({ |
| "facets": 55 |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid type: integer `55`, expected a map" |
| ), |
| |
| // constraints |
| test_cml_rights_all( |
| json!({ |
| "use": [ |
| { |
| "directory": "mydir", |
| "path": "/mydir", |
| "rights": ["connect", "enumerate", "read_bytes", "write_bytes", |
| "execute", "update_attributes", "get_attributes", "traverse", |
| "modify_directory"], |
| }, |
| ] |
| }), |
| Ok(()) |
| ), |
| test_cml_rights_invalid( |
| json!({ |
| "use": [ |
| { |
| "directory": "mydir", |
| "path": "/mydir", |
| "rights": ["cAnnect", "enumerate"], |
| }, |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "unknown variant `cAnnect`, expected one of `connect`, `enumerate`, `execute`, `get_attributes`, `modify_directory`, `read_bytes`, `traverse`, `update_attributes`, `write_bytes`, `r*`, `w*`, `x*`, `rw*`, `rx*`" |
| ), |
| test_cml_rights_duplicate( |
| json!({ |
| "use": [ |
| { |
| "directory": "mydir", |
| "path": "/mydir", |
| "rights": ["connect", "connect"], |
| }, |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: array with duplicate element, expected a nonempty array of rights, with unique elements" |
| ), |
| test_cml_rights_empty( |
| json!({ |
| "use": [ |
| { |
| "directory": "mydir", |
| "path": "/mydir", |
| "rights": [], |
| }, |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected a nonempty array of rights, with unique elements" |
| ), |
| test_cml_rights_alias_star_expansion( |
| json!({ |
| "use": [ |
| { |
| "directory": "mydir", |
| "rights": ["r*"], |
| "path": "/mydir", |
| }, |
| ] |
| }), |
| Ok(()) |
| ), |
| test_cml_rights_alias_star_expansion_with_longform( |
| json!({ |
| "use": [ |
| { |
| "directory": "mydir", |
| "rights": ["w*", "read_bytes"], |
| "path": "/mydir", |
| }, |
| ] |
| }), |
| Ok(()) |
| ), |
| test_cml_rights_alias_star_expansion_with_longform_collision( |
| json!({ |
| "use": [ |
| { |
| "directory": "mydir", |
| "path": "/mydir", |
| "rights": ["r*", "read_bytes"], |
| }, |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"read_bytes\" is duplicated in the rights clause." |
| ), |
| test_cml_rights_alias_star_expansion_collision( |
| json!({ |
| "use": [ |
| { |
| "directory": "mydir", |
| "path": "/mydir", |
| "rights": ["w*", "x*"], |
| }, |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"x*\" is duplicated in the rights clause." |
| ), |
| test_cml_rights_use_invalid( |
| json!({ |
| "use": [ |
| { "directory": "mydir", "path": "/mydir" }, |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Rights required for this use statement." |
| ), |
| test_cml_path( |
| json!({ |
| "capabilities": [ |
| { |
| "protocol": "foo", |
| "path": "/foo/?!@#$%/Bar", |
| }, |
| ] |
| }), |
| Ok(()) |
| ), |
| test_cml_path_invalid_empty( |
| json!({ |
| "capabilities": [ |
| { "protocol": "foo", "path": "" }, |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected a non-empty path no more than 1024 characters in length" |
| ), |
| test_cml_path_invalid_root( |
| json!({ |
| "capabilities": [ |
| { "protocol": "foo", "path": "/" }, |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/\", expected a path with leading `/` and non-empty segments" |
| ), |
| test_cml_path_invalid_absolute_is_relative( |
| json!({ |
| "capabilities": [ |
| { "protocol": "foo", "path": "foo/bar" }, |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"foo/bar\", expected a path with leading `/` and non-empty segments" |
| ), |
| test_cml_path_invalid_trailing( |
| json!({ |
| "capabilities": [ |
| { "protocol": "foo", "path":"/foo/bar/" }, |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/foo/bar/\", expected a path with leading `/` and non-empty segments" |
| ), |
| test_cml_path_too_long( |
| json!({ |
| "capabilities": [ |
| { "protocol": "foo", "path": format!("/{}", "a".repeat(1024)) }, |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid length 1025, expected a non-empty path no more than 1024 characters in length" |
| ), |
| test_cml_relative_path( |
| json!({ |
| "use": [ |
| { |
| "directory": "foo", |
| "path": "/foo", |
| "rights": ["r*"], |
| "subdir": "?!@#$%/Bar", |
| }, |
| ] |
| }), |
| Ok(()) |
| ), |
| test_cml_relative_path_invalid_empty( |
| json!({ |
| "use": [ |
| { |
| "directory": "foo", |
| "path": "/foo", |
| "rights": ["r*"], |
| "subdir": "", |
| }, |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected a non-empty path no more than 1024 characters in length" |
| ), |
| test_cml_relative_path_invalid_root( |
| json!({ |
| "use": [ |
| { |
| "directory": "foo", |
| "path": "/foo", |
| "rights": ["r*"], |
| "subdir": "/", |
| }, |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/\", expected a path with no leading `/` and non-empty segments" |
| ), |
| test_cml_relative_path_invalid_absolute( |
| json!({ |
| "use": [ |
| { |
| "directory": "foo", |
| "path": "/foo", |
| "rights": ["r*"], |
| "subdir": "/bar", |
| }, |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/bar\", expected a path with no leading `/` and non-empty segments" |
| ), |
| test_cml_relative_path_invalid_trailing( |
| json!({ |
| "use": [ |
| { |
| "directory": "foo", |
| "path": "/foo", |
| "rights": ["r*"], |
| "subdir": "bar/", |
| }, |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"bar/\", expected a path with no leading `/` and non-empty segments" |
| ), |
| test_cml_relative_path_too_long( |
| json!({ |
| "use": [ |
| { |
| "directory": "foo", |
| "path": "/foo", |
| "rights": ["r*"], |
| "subdir": format!("{}", "a".repeat(1025)), |
| }, |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid length 1025, expected a non-empty path no more than 1024 characters in length" |
| ), |
| test_cml_relative_ref_too_long( |
| json!({ |
| "expose": [ |
| { |
| "protocol": "fuchsia.logger.Log", |
| "from": &format!("#{}", "a".repeat(101)), |
| }, |
| ], |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| }, |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid length 102, expected one or an array of \"framework\", \"self\", or \"#<child-name>\"" |
| ), |
| test_cml_capability_name( |
| json!({ |
| "use": [ |
| { |
| "protocol": "abcdefghijklmnopqrstuvwxyz0123456789_-.", |
| }, |
| ] |
| }), |
| Ok(()) |
| ), |
| test_cml_capability_name_invalid( |
| json!({ |
| "use": [ |
| { |
| "protocol": "/bad", |
| }, |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/bad\", expected a name or nonempty array of names, with unique elements" |
| ), |
| test_cml_child_name( |
| json!({ |
| "children": [ |
| { |
| "name": "abcdefghijklmnopqrstuvwxyz0123456789_-.", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| }, |
| ] |
| }), |
| Ok(()) |
| ), |
| test_cml_child_name_invalid( |
| json!({ |
| "children": [ |
| { |
| "name": "/bad", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| }, |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"/bad\", expected a \ |
| name that consists of [A-Za-z0-9_.-] and starts with [A-Za-z0-9_]" |
| ), |
| test_cml_child_name_too_long( |
| json!({ |
| "children": [ |
| { |
| "name": "a".repeat(101), |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| } |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid length 101, expected a non-empty name no more than 100 characters in length" |
| ), |
| test_cml_url( |
| json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "my+awesome-scheme.2://abc123!@#$%.com", |
| }, |
| ] |
| }), |
| Ok(()) |
| ), |
| test_cml_url_invalid( |
| json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg", |
| }, |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"fuchsia-pkg\", expected a valid URL" |
| ), |
| test_cml_url_too_long( |
| json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": &format!("fuchsia-pkg://{}", "a".repeat(4083)), |
| }, |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid length 4097, expected a non-empty URL no more than 4096 characters in length" |
| ), |
| test_cml_duplicate_identifiers_children_collection( |
| json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm" |
| } |
| ], |
| "collections": [ |
| { |
| "name": "logger", |
| "durability": "transient" |
| } |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "identifier \"logger\" is defined twice, once in \"collections\" and once in \"children\"" |
| ), |
| test_cml_duplicate_identifiers_children_storage( |
| json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm" |
| } |
| ], |
| "capabilities": [ |
| { |
| "storage": "logger", |
| "path": "/logs", |
| "from": "parent" |
| } |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "identifier \"logger\" is defined twice, once in \"storage\" and once in \"children\"" |
| ), |
| test_cml_duplicate_identifiers_collection_storage( |
| json!({ |
| "collections": [ |
| { |
| "name": "logger", |
| "durability": "transient" |
| } |
| ], |
| "capabilities": [ |
| { |
| "storage": "logger", |
| "path": "/logs", |
| "from": "parent" |
| } |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "identifier \"logger\" is defined twice, once in \"storage\" and once in \"collections\"" |
| ), |
| test_cml_duplicate_identifiers_children_runners( |
| json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm" |
| } |
| ], |
| "capabilities": [ |
| { |
| "runner": "logger", |
| "from": "parent" |
| } |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "identifier \"logger\" is defined twice, once in \"runners\" and once in \"children\"" |
| ), |
| test_cml_duplicate_identifiers_environments( |
| json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm" |
| } |
| ], |
| "environments": [ |
| { |
| "name": "logger", |
| } |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "identifier \"logger\" is defined twice, once in \"environments\" and once in \"children\"" |
| ), |
| |
| // deny unknown fields |
| test_deny_unknown_fields( |
| json!( |
| { |
| "program": { |
| "runner": "elf", |
| "binary": "bin/app", |
| }, |
| "unknown_field": {}, |
| } |
| ), |
| Err(Error::Parse { err, .. }) if err.starts_with("unknown field `unknown_field`, expected one of ") |
| ), |
| test_offer_source_availability_unknown( |
| json!({ |
| "children": [ |
| { |
| "name": "foo", |
| "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm" |
| }, |
| ], |
| "offer": [ |
| { |
| "protocol": "fuchsia.examples.Echo", |
| "from": "#bar", |
| "to": "#foo", |
| "availability": "optional", |
| "source_availability": "unknown", |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_offer_source_availability_required( |
| json!({ |
| "children": [ |
| { |
| "name": "foo", |
| "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm" |
| }, |
| ], |
| "offer": [ |
| { |
| "protocol": "fuchsia.examples.Echo", |
| "from": "#bar", |
| "to": "#foo", |
| "source_availability": "required", |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"offer\" source \"#bar\" does not appear in \"children\" or \"capabilities\"" |
| ), |
| test_offer_source_availability_omitted( |
| json!({ |
| "children": [ |
| { |
| "name": "foo", |
| "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm" |
| }, |
| ], |
| "offer": [ |
| { |
| "protocol": "fuchsia.examples.Echo", |
| "from": "#bar", |
| "to": "#foo", |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"offer\" source \"#bar\" does not appear in \"children\" or \"capabilities\"" |
| ), |
| test_cml_use_invalid_availability( |
| json!({ |
| "use": [ |
| { |
| "protocol": "fuchsia.examples.Echo", |
| "availability": "same_as_target", |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"availability: same_as_target\" cannot be used with use declarations" |
| ), |
| test_offer_source_void_availability_required( |
| json!({ |
| "children": [ |
| { |
| "name": "foo", |
| "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm" |
| }, |
| ], |
| "offer": [ |
| { |
| "protocol": "fuchsia.examples.Echo", |
| "from": "void", |
| "to": "#foo", |
| "availability": "required", |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "capabilities with a source of \"void\" must have an availability of \"optional\"" |
| ), |
| test_offer_source_void_availability_same_as_target( |
| json!({ |
| "children": [ |
| { |
| "name": "foo", |
| "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm" |
| }, |
| ], |
| "offer": [ |
| { |
| "protocol": "fuchsia.examples.Echo", |
| "from": "void", |
| "to": "#foo", |
| "availability": "same_as_target", |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "capabilities with a source of \"void\" must have an availability of \"optional\"" |
| ), |
| test_offer_source_missing_availability_required( |
| json!({ |
| "children": [ |
| { |
| "name": "foo", |
| "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm" |
| }, |
| ], |
| "offer": [ |
| { |
| "protocol": "fuchsia.examples.Echo", |
| "from": "#bar", |
| "to": "#foo", |
| "availability": "required", |
| "source_availability": "unknown", |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "capabilities with an intentionally missing source must have an availability that is either unset or \"optional\"" |
| ), |
| test_offer_source_missing_availability_same_as_target( |
| json!({ |
| "children": [ |
| { |
| "name": "foo", |
| "url": "fuchsia-pkg://foo.com/foo#meta/foo.cm" |
| }, |
| ], |
| "offer": [ |
| { |
| "protocol": "fuchsia.examples.Echo", |
| "from": "#bar", |
| "to": "#foo", |
| "availability": "same_as_target", |
| "source_availability": "unknown", |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "capabilities with an intentionally missing source must have an availability that is either unset or \"optional\"" |
| ), |
| } |
| |
| // Tests for services. |
| test_validate_cml! { |
| test_cml_validate_use_service( |
| json!({ |
| "use": [ |
| { "service": "CoolFonts", "path": "/svc/fuchsia.fonts.Provider" }, |
| { "service": "fuchsia.component.Realm", "from": "framework" }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_use_as_with_service( |
| json!({ |
| "use": [ { "service": "foo", "as": "xxx" } ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"as\" cannot be used with \"service\"" |
| ), |
| test_cml_use_invalid_from_with_service( |
| json!({ |
| "use": [ { "service": "foo", "from": "debug" } ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "only \"protocol\" supports source from \"debug\"" |
| ), |
| test_cml_validate_offer_service( |
| json!({ |
| "offer": [ |
| { |
| "service": "fuchsia.logger.Log", |
| "from": "#logger", |
| "to": [ "#echo_server", "#modular" ], |
| "as": "fuchsia.logger.SysLog" |
| }, |
| { |
| "service": "fuchsia.fonts.Provider", |
| "from": "parent", |
| "to": [ "#echo_server" ] |
| }, |
| { |
| "service": "fuchsia.net.Netstack", |
| "from": "self", |
| "to": [ "#echo_server" ] |
| }, |
| ], |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://logger", |
| }, |
| { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://echo_server", |
| } |
| ], |
| "collections": [ |
| { |
| "name": "modular", |
| "durability": "persistent", |
| }, |
| ], |
| "capabilities": [ |
| { "service": "fuchsia.net.Netstack" }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_offer_service_multiple_from( |
| json!({ |
| "offer": [ |
| { |
| "service": "fuchsia.logger.Log", |
| "from": [ "#logger", "parent" ], |
| "to": [ "#echo_server" ], |
| }, |
| ], |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm" |
| }, |
| { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm" |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_offer_service_from_collection_ok( |
| json!({ |
| "collections": [ { |
| "name": "coll", |
| "durability": "transient", |
| } ], |
| "children": [ { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm", |
| } ], |
| "offer": [ { |
| "service": "fuchsia.logger.Log", "from": "#coll", "to": [ "#echo_server" ] |
| }] |
| }), |
| Ok(()) |
| ), |
| test_cml_offer_service_from_self_missing( |
| json!({ |
| "offer": [ |
| { |
| "service": "pkg_service", |
| "from": "self", |
| "to": [ "#modular" ], |
| }, |
| ], |
| "children": [ |
| { |
| "name": "modular", |
| "url": "fuchsia-pkg://fuchsia.com/modular#meta/modular.cm" |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Service \"pkg_service\" is offered from self, so it must be declared as a \"service\" in \"capabilities\"" |
| ), |
| test_cml_validate_expose_service( |
| json!( |
| { |
| "expose": [ |
| { |
| "service": "fuchsia.fonts.Provider", |
| "from": "self", |
| }, |
| { |
| "service": "fuchsia.logger.Log", |
| "from": "#logger", |
| "as": "logger" |
| }, |
| ], |
| "capabilities": [ |
| { "service": "fuchsia.fonts.Provider" }, |
| ], |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://logger", |
| }, |
| ] |
| } |
| ), |
| Ok(()) |
| ), |
| test_cml_expose_service_multiple_from( |
| json!({ |
| "expose": [ |
| { |
| "service": "fuchsia.logger.Log", |
| "from": [ "#logger", "self" ], |
| }, |
| ], |
| "capabilities": [ |
| { "service": "fuchsia.logger.Log" }, |
| ], |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_expose_service_from_self_missing( |
| json!({ |
| "expose": [ |
| { |
| "service": "pkg_service", |
| "from": "self", |
| }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "Service \"pkg_service\" is exposed from self, so it must be declared as a \"service\" in \"capabilities\"" |
| ), |
| test_cml_expose_service_from_collection_ok( |
| json!({ |
| "collections": [ { |
| "name": "coll", |
| "durability": "transient", |
| } ], |
| "expose": [ { |
| "service": "fuchsia.logger.Log", "from": "#coll" |
| }] |
| }), |
| Ok(()) |
| ), |
| test_cml_service( |
| json!({ |
| "capabilities": [ |
| { |
| "protocol": "a", |
| "path": "/minfs", |
| }, |
| { |
| "protocol": "b", |
| "path": "/data", |
| }, |
| { |
| "protocol": "c", |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_service_multi( |
| json!({ |
| "capabilities": [ |
| { |
| "service": ["a", "b", "c"], |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_service_multi_invalid_path( |
| json!({ |
| "capabilities": [ |
| { |
| "service": ["a", "b", "c"], |
| "path": "/minfs", |
| }, |
| ], |
| }), |
| Err(Error::Validate { err, .. }) if &err == "\"path\" can only be specified when one `service` is supplied." |
| ), |
| test_cml_service_all_valid_chars( |
| json!({ |
| "capabilities": [ |
| { |
| "service": "abcdefghijklmnopqrstuvwxyz0123456789_-service", |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| } |
| |
| // Tests structured config when the feature is enabled |
| test_validate_cml_with_feature! { FeatureSet::from(vec![Feature::StructuredConfig]), { |
| test_cml_configs( |
| json!({ |
| "config": { |
| "verbosity": { |
| "type": "string", |
| "max_size": 20, |
| }, |
| "timeout": { "type": "uint64" }, |
| "tags": { |
| "type": "vector", |
| "max_count": 10, |
| "element": { |
| "type": "string", |
| "max_size": 50 |
| } |
| } |
| } |
| }), |
| Ok(()) |
| ), |
| |
| test_cml_configs_not_object( |
| json!({ |
| "config": "abcd" |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid type: string \"abcd\", expected a map" |
| ), |
| |
| test_cml_configs_empty( |
| json!({ |
| "config": { |
| } |
| }), |
| Err(Error::Validate { err, .. }) if &err == "'config' section is empty" |
| ), |
| |
| test_cml_configs_bad_type( |
| json!({ |
| "config": { |
| "verbosity": 123456 |
| } |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid type: integer `123456`, expected internally tagged enum" |
| ), |
| |
| test_cml_configs_unknown_type( |
| json!({ |
| "config": { |
| "verbosity": { |
| "type": "foo" |
| } |
| } |
| }), |
| Err(Error::Parse { err, .. }) if &err == "unknown variant `foo`, expected one of `bool`, `uint8`, `uint16`, `uint32`, `uint64`, `int8`, `int16`, `int32`, `int64`, `string`, `vector`" |
| ), |
| |
| test_cml_configs_no_max_count_vector( |
| json!({ |
| "config": { |
| "tags": { |
| "type": "vector", |
| "element": { |
| "type": "string", |
| "max_size": 50, |
| } |
| } |
| } |
| }), |
| Err(Error::Parse { err, .. }) if &err == "missing field `max_count`" |
| ), |
| |
| test_cml_configs_no_max_size_string( |
| json!({ |
| "config": { |
| "verbosity": { |
| "type": "string", |
| } |
| } |
| }), |
| Err(Error::Parse { err, .. }) if &err == "missing field `max_size`" |
| ), |
| |
| test_cml_configs_no_max_size_string_vector( |
| json!({ |
| "config": { |
| "tags": { |
| "type": "vector", |
| "max_count": 10, |
| "element": { |
| "type": "string", |
| } |
| } |
| } |
| }), |
| Err(Error::Parse { err, .. }) if &err == "missing field `max_size`" |
| ), |
| |
| test_cml_configs_empty_key( |
| json!({ |
| "config": { |
| "": { |
| "type": "bool" |
| } |
| } |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected a non-empty name no more than 64 characters in length" |
| ), |
| |
| test_cml_configs_too_long_key( |
| json!({ |
| "config": { |
| "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { |
| "type": "bool" |
| } |
| } |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid length 74, expected a non-empty name no more than 64 characters in length" |
| ), |
| test_cml_configs_key_starts_with_number( |
| json!({ |
| "config": { |
| "8abcd": { "type": "uint8" } |
| } |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"8abcd\", expected a name which must start with a letter, can contain letters, numbers, and underscores, but cannot end with an underscore" |
| ), |
| |
| test_cml_configs_key_ends_with_underscore( |
| json!({ |
| "config": { |
| "abcd_": { "type": "uint8" } |
| } |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"abcd_\", expected a name which must start with a letter, can contain letters, numbers, and underscores, but cannot end with an underscore" |
| ), |
| |
| test_cml_configs_capitals_in_key( |
| json!({ |
| "config": { |
| "ABCD": { "type": "uint8" } |
| } |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"ABCD\", expected a name which must start with a letter, can contain letters, numbers, and underscores, but cannot end with an underscore" |
| ), |
| |
| test_cml_configs_special_chars_in_key( |
| json!({ |
| "config": { |
| "!@#$": { "type": "uint8" } |
| } |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"!@#$\", expected a name which must start with a letter, can contain letters, numbers, and underscores, but cannot end with an underscore" |
| ), |
| |
| test_cml_configs_dashes_in_key( |
| json!({ |
| "config": { |
| "abcd-efgh": { "type": "uint8" } |
| } |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"abcd-efgh\", expected a name which must start with a letter, can contain letters, numbers, and underscores, but cannot end with an underscore" |
| ), |
| |
| test_cml_configs_bad_max_size_string( |
| json!({ |
| "config": { |
| "verbosity": { |
| "type": "string", |
| "max_size": "abcd" |
| } |
| } |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid type: string \"abcd\", expected u32" |
| ), |
| |
| test_cml_configs_zero_max_size_string( |
| json!({ |
| "config": { |
| "verbosity": { |
| "type": "string", |
| "max_size": 0 |
| } |
| } |
| }), |
| Err(Error::Parse { err, .. }) if &err == "expected a non-zero value" |
| ), |
| |
| test_cml_configs_bad_max_count_on_vector( |
| json!({ |
| "config": { |
| "toggles": { |
| "type": "vector", |
| "max_count": "abcd", |
| "element": { |
| "type": "bool" |
| } |
| } |
| } |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid type: string \"abcd\", expected u32" |
| ), |
| |
| test_cml_configs_zero_max_count_on_vector( |
| json!({ |
| "config": { |
| "toggles": { |
| "type": "vector", |
| "max_count": 0, |
| "element": { |
| "type": "bool" |
| } |
| } |
| } |
| }), |
| Err(Error::Parse { err, .. }) if &err == "expected a non-zero value" |
| ), |
| |
| test_cml_configs_bad_max_size_string_vector( |
| json!({ |
| "config": { |
| "toggles": { |
| "type": "vector", |
| "max_count": 100, |
| "element": { |
| "type": "string", |
| "max_size": "abcd" |
| } |
| } |
| } |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid type: string \"abcd\", expected u32" |
| ), |
| |
| test_cml_configs_zero_max_size_string_vector( |
| json!({ |
| "config": { |
| "toggles": { |
| "type": "vector", |
| "max_count": 100, |
| "element": { |
| "type": "string", |
| "max_size": 0 |
| } |
| } |
| } |
| }), |
| Err(Error::Parse { err, .. }) if &err == "expected a non-zero value" |
| ), |
| }} |
| |
| // Tests structured config when the feature is disabled |
| test_validate_cml! { |
| test_cml_config_without_config_feature( |
| json!({ |
| "config": { |
| "verbosity": { |
| "type": "string", |
| "max_size": 50 |
| }, |
| "timeout": { "type": "uint64" }, |
| } |
| }), |
| Err(Error::RestrictedFeature(s)) if s == "structured_config" |
| ), |
| } |
| |
| // Tests the use of `allowed_offers` when the "DynamicOffers" feature is set. |
| test_validate_cml_with_feature! { FeatureSet::from(vec![Feature::DynamicOffers]), { |
| test_cml_validate_set_allowed_offers_static( |
| json!({ |
| "collections": [ |
| { |
| "name": "foo", |
| "durability": "transient", |
| "allowed_offers": "static_only" |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_validate_set_allowed_offers_dynamic( |
| json!({ |
| "collections": [ |
| { |
| "name": "foo", |
| "durability": "transient", |
| "allowed_offers": "static_and_dynamic" |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| }} |
| |
| // Tests that the use of `allowed_offers` fails when the "DynamicOffers" |
| // feature is not set. |
| test_validate_cml! { |
| test_cml_static_only_without_feature( |
| json!({ |
| "collections": [ |
| { |
| "name": "foo", |
| "durability": "transient", |
| "allowed_offers": "static_only" |
| }, |
| ], |
| }), |
| Err(Error::RestrictedFeature(s)) if s == "dynamic_offers" |
| ), |
| test_cml_static_and_dynamic_offers_without_feature( |
| json!({ |
| "collections": [ |
| { |
| "name": "foo", |
| "durability": "transient", |
| "allowed_offers": "static_and_dynamic" |
| }, |
| ], |
| }), |
| Err(Error::RestrictedFeature(s)) if s == "dynamic_offers" |
| ), |
| } |
| |
| // Tests the use of hub when the "hub" feature is set. |
| test_validate_cml_with_feature! { FeatureSet::from(vec![Feature::Hub]), { |
| test_cml_use_hub( |
| json!({ |
| "use": [ |
| { |
| "directory": "hub", |
| "from": "framework", |
| "rights": [ "r*" ], |
| "path": "/hub", |
| }, |
| ], |
| "expose": [ |
| { "directory": "hub", "from": "framework" }, |
| ], |
| "offer": [ |
| { "directory": "hub", "from": "framework", "to": "#child" }, |
| ], |
| "children": [ |
| { "name": "child", "url": "fuchsia-pkg://child" } |
| ], |
| }), |
| Ok(()) |
| ), |
| }} |
| |
| // Tests the use of hub fails when the "hub" feature is not set. |
| test_validate_cml! { |
| test_cml_use_hub_without_feature( |
| json!({ |
| "use": [ |
| { |
| "directory": "hub", |
| "from": "framework", |
| "rights": [ "r*" ], |
| "path": "/hub", |
| }, |
| ], |
| }), |
| Err(Error::RestrictedFeature(s)) if s == "hub" |
| ), |
| test_cml_expose_hub_without_feature( |
| json!({ |
| "expose": [ |
| { "directory": "hub", "from": "framework" }, |
| ], |
| }), |
| Err(Error::RestrictedFeature(s)) if s == "hub" |
| ), |
| test_cml_offer_hub_without_feature( |
| json!({ |
| "offer": [ |
| { "directory": "hub", "from": "framework", "to": "#child" }, |
| ], |
| "children": [ |
| { "name": "child", "url": "fuchsia-pkg://child" } |
| ], |
| }), |
| Err(Error::RestrictedFeature(s)) if s == "hub" |
| ), |
| } |
| |
| // Tests the use of `allow_long_names` when the "AllowLongNames" feature is set. |
| test_validate_cml_with_feature! { FeatureSet::from(vec![Feature::AllowLongNames]), { |
| test_cml_validate_set_allow_long_names_true( |
| json!({ |
| "collections": [ |
| { |
| "name": "foo", |
| "durability": "transient", |
| "allow_long_names": true |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| test_cml_validate_set_allow_long_names_false( |
| json!({ |
| "collections": [ |
| { |
| "name": "foo", |
| "durability": "transient", |
| "allow_long_names": false |
| }, |
| ], |
| }), |
| Ok(()) |
| ), |
| }} |
| |
| // Tests that the use of `allow_long_names` fails when the "AllowLongNames" |
| // feature is not set. |
| test_validate_cml! { |
| test_cml_allow_long_names_without_feature( |
| json!({ |
| "collections": [ |
| { |
| "name": "foo", |
| "durability": "transient", |
| "allow_long_names": true |
| }, |
| ], |
| }), |
| Err(Error::RestrictedFeature(s)) if s == "allow_long_names" |
| ), |
| } |
| |
| test_validate_cmx! { |
| test_cmx_err_empty_json( |
| json!({}), |
| Err(Error::Validate { schema_name: Some(s), err, .. }) if s == *CMX_SCHEMA.name && &err == "This property is required at /program" |
| ), |
| test_cmx_program( |
| json!({"program": { "binary": "bin/app" }}), |
| Ok(()) |
| ), |
| test_cmx_program_no_binary( |
| json!({ "program": {}}), |
| Err(Error::Validate { schema_name: Some(s), err, .. }) if s == *CMX_SCHEMA.name && &err == "OneOf conditions are not met at /program" |
| ), |
| test_cmx_bad_program( |
| json!({"prigram": { "binary": "bin/app" }}), |
| Err(Error::Validate { schema_name: Some(s), err, .. }) if s == *CMX_SCHEMA.name && &err == "Property conditions are not met at , This property is required at /program" |
| ), |
| test_cmx_sandbox( |
| json!({ |
| "program": { "binary": "bin/app" }, |
| "sandbox": { "dev": [ "class/camera" ] } |
| }), |
| Ok(()) |
| ), |
| test_cmx_facets( |
| json!({ |
| "program": { "binary": "bin/app" }, |
| "facets": { |
| "fuchsia.test": { |
| "system-services": [ "fuchsia.logger.LogSink" ] |
| } |
| } |
| }), |
| Ok(()) |
| ), |
| test_cmx_block_system_data( |
| json!({ |
| "program": { "binary": "bin/app" }, |
| "sandbox": { |
| "system": [ "data" ] |
| } |
| }), |
| Err(Error::Validate { schema_name: Some(s), err, .. }) if s == *CMX_SCHEMA.name && &err == "Not condition is not met at /sandbox/system/0" |
| ), |
| test_cmx_block_system_data_stem( |
| json!({ |
| "program": { "binary": "bin/app" }, |
| "sandbox": { |
| "system": [ "data-should-pass" ] |
| } |
| }), |
| Ok(()) |
| ), |
| test_cmx_block_system_data_leading_slash( |
| json!({ |
| "program": { "binary": "bin/app" }, |
| "sandbox": { |
| "system": [ "/data" ] |
| } |
| }), |
| Err(Error::Validate { schema_name: Some(s), err, .. }) if s == *CMX_SCHEMA.name && &err == "Not condition is not met at /sandbox/system/0" |
| ), |
| test_cmx_block_system_data_subdir( |
| json!({ |
| "program": { "binary": "bin/app" }, |
| "sandbox": { |
| "system": [ "data/should-fail" ] |
| } |
| }), |
| Err(Error::Validate { schema_name: Some(s), err, .. }) if s == *CMX_SCHEMA.name && &err == "Not condition is not met at /sandbox/system/0" |
| ), |
| test_cmx_block_system_deprecated_data( |
| json!({ |
| "program": { "binary": "bin/app" }, |
| "sandbox": { |
| "system": [ "deprecated-data" ] |
| } |
| }), |
| Err(Error::Validate { schema_name: Some(s), err, .. }) if s == *CMX_SCHEMA.name && &err == "Not condition is not met at /sandbox/system/0" |
| ), |
| test_cmx_block_system_deprecated_data_stem( |
| json!({ |
| "program": { "binary": "bin/app" }, |
| "sandbox": { |
| "system": [ "deprecated-data-should-pass" ] |
| } |
| }), |
| Ok(()) |
| ), |
| test_cmx_block_system_deprecated_data_leading_slash( |
| json!({ |
| "program": { "binary": "bin/app" }, |
| "sandbox": { |
| "system": [ "/deprecated-data" ] |
| } |
| }), |
| Err(Error::Validate { schema_name: Some(s), err, .. }) if s == *CMX_SCHEMA.name && &err == "Not condition is not met at /sandbox/system/0" |
| ), |
| test_cmx_block_system_deprecated_data_subdir( |
| json!({ |
| "program": { "binary": "bin/app" }, |
| "sandbox": { |
| "system": [ "deprecated-data/should-fail" ] |
| } |
| }), |
| Err(Error::Validate { schema_name: Some(s), err, .. }) if s == *CMX_SCHEMA.name && &err == "Not condition is not met at /sandbox/system/0" |
| ), |
| } |
| |
| // We can't simply using JsonSchema::new here and create a temp file with the schema content |
| // to pass to validate() later because the path in the errors in the expected results below |
| // need to include the whole path, since that's what you get in the Error::Validate. |
| lazy_static! { |
| static ref BLOCK_SHELL_FEATURE_SCHEMA: JsonSchema<'static> = str_to_json_schema( |
| "block_shell_feature.json", |
| include_str!("../test_block_shell_feature.json") |
| ); |
| } |
| lazy_static! { |
| static ref BLOCK_DEV_SCHEMA: JsonSchema<'static> = |
| str_to_json_schema("block_dev.json", include_str!("../test_block_dev.json")); |
| } |
| |
| fn str_to_json_schema<'a, 'b>(name: &'a str, content: &'a str) -> JsonSchema<'b> { |
| lazy_static! { |
| static ref TEMPDIR: TempDir = TempDir::new().unwrap(); |
| } |
| |
| let tmp_path = TEMPDIR.path().join(name); |
| File::create(&tmp_path).unwrap().write_all(content.as_bytes()).unwrap(); |
| JsonSchema::new_from_file(&tmp_path).unwrap() |
| } |
| |
| macro_rules! test_validate_extra_schemas { |
| ( |
| $( |
| $test_name:ident($input:expr, $extra_schemas:expr, $($pattern:tt)+), |
| )+ |
| ) => { |
| $( |
| #[test] |
| fn $test_name() -> Result<(), Error> { |
| let tmp_dir = TempDir::new()?; |
| let tmp_cmx_path = tmp_dir.path().join("test.cmx"); |
| let input = format!("{}", $input); |
| File::create(&tmp_cmx_path)?.write_all(input.as_bytes())?; |
| let extra_schemas: &[(&JsonSchema<'_>, Option<String>)] = $extra_schemas; |
| let extra_schema_paths: Vec<_> = extra_schemas |
| .iter() |
| .map(|i| (Path::new(&*i.0.name), i.1.clone())) |
| .collect(); |
| let result = validate(&[tmp_cmx_path.as_path()], &extra_schema_paths, &FeatureSet::empty()); |
| assert_matches!(result, $($pattern)+); |
| Ok(()) |
| } |
| )+ |
| } |
| } |
| |
| test_validate_extra_schemas! { |
| test_validate_extra_schemas_empty_json( |
| json!({"program": {"binary": "a"}}), |
| &[(&BLOCK_SHELL_FEATURE_SCHEMA, None)], |
| Ok(()) |
| ), |
| test_validate_extra_schemas_empty_features( |
| json!({"sandbox": {"features": []}, "program": {"binary": "a"}}), |
| &[(&BLOCK_SHELL_FEATURE_SCHEMA, None)], |
| Ok(()) |
| ), |
| test_validate_extra_schemas_feature_not_present( |
| json!({"sandbox": {"features": ["isolated-persistent-storage"]}, "program": {"binary": "a"}}), |
| &[(&BLOCK_SHELL_FEATURE_SCHEMA, None)], |
| Ok(()) |
| ), |
| test_validate_extra_schemas_feature_present( |
| json!({"sandbox": {"features" : ["deprecated-shell"]}, "program": {"binary": "a"}}), |
| &[(&BLOCK_SHELL_FEATURE_SCHEMA, None)], |
| Err(Error::Validate { schema_name: Some(s), err, .. }) if *s == BLOCK_SHELL_FEATURE_SCHEMA.name && &err == "Not condition is not met at /sandbox/features/0" |
| ), |
| test_validate_extra_schemas_block_dev( |
| json!({"dev": ["misc"], "program": {"binary": "a"}}), |
| &[(&BLOCK_DEV_SCHEMA, None)], |
| Err(Error::Validate { schema_name: Some(s), err, .. }) if *s == BLOCK_DEV_SCHEMA.name && &err == "Not condition is not met at /dev" |
| ), |
| test_validate_multiple_extra_schemas_valid( |
| json!({"sandbox": {"features": ["isolated-persistent-storage"]}, "program": {"binary": "a"}}), |
| &[(&BLOCK_SHELL_FEATURE_SCHEMA, None), (&BLOCK_DEV_SCHEMA, None)], |
| Ok(()) |
| ), |
| test_validate_multiple_extra_schemas_invalid( |
| json!({"dev": ["misc"], "sandbox": {"features": ["isolated-persistent-storage"]}, "program": {"binary": "a"}}), |
| &[(&BLOCK_SHELL_FEATURE_SCHEMA, None), (&BLOCK_DEV_SCHEMA, None)], |
| Err(Error::Validate { schema_name: Some(s), err, .. }) if *s == BLOCK_DEV_SCHEMA.name && &err == "Not condition is not met at /dev" |
| ), |
| test_validate_extra_error( |
| json!({"dev": ["misc"], "program": {"binary": "a"}}), |
| &[(&BLOCK_DEV_SCHEMA, Some("Extra error".to_string()))], |
| Err(Error::Validate { schema_name: Some(s), err, .. }) if *s == BLOCK_DEV_SCHEMA.name && &err == "Not condition is not met at /dev\nExtra error" |
| ), |
| } |
| } |