| // 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::error::{Error, Location}, |
| crate::include, |
| crate::{cml, one_or_many::OneOrMany}, |
| cm_json::{JsonSchema, CMX_SCHEMA}, |
| directed_graph::{self, DirectedGraph}, |
| serde_json::Value, |
| serde_json5, |
| std::{ |
| collections::{HashMap, HashSet}, |
| fmt, |
| fs::File, |
| hash::Hash, |
| io::Read, |
| iter, |
| path::{Path, PathBuf}, |
| }, |
| 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>)], |
| ) -> 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)?; |
| } |
| Ok(()) |
| } |
| |
| /// Read in and parse .cml file. Returns a cml::Document if the file is valid, or an Error if not. |
| pub fn parse_cml(file: &Path, includepath: Option<&PathBuf>) -> Result<cml::Document, Error> { |
| let mut document: cml::Document = read_cml(file)?; |
| |
| if let Some(include_path) = includepath { |
| for include in include::transitive_includes(&file.into(), include_path)? { |
| let mut include_document = read_cml(&include_path.join(&include))?; |
| document.merge_from(&mut include_document); |
| } |
| } |
| |
| let mut ctx = ValidationContext::new(&document); |
| let mut res = ctx.validate(); |
| if let Err(Error::Validate { filename, .. }) = &mut res { |
| *filename = Some(file.to_string_lossy().into_owned()); |
| } |
| res.and(Ok(document)) |
| } |
| |
| fn read_cml(file: &Path) -> Result<cml::Document, Error> { |
| let mut buffer = String::new(); |
| File::open(&file) |
| .map_err(|e| { |
| Error::parse(format!("Couldn't read include {:?}: {}", file, e), None, Some(file)) |
| })? |
| .read_to_string(&mut buffer) |
| .map_err(|e| { |
| Error::parse(format!("Couldn't read include {:?}: {}", file, e), None, Some(file)) |
| })?; |
| serde_json5::from_str(&buffer).map_err(|e| { |
| let serde_json5::Error::Message { location, msg } = e; |
| let location = location.map(|l| Location { line: l.line, column: l.column }); |
| Error::parse(msg, location, Some(file)) |
| }) |
| } |
| |
| /// 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>)], |
| ) -> 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") => { |
| parse_cml(file, None)?; |
| } |
| _ => { |
| 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, |
| 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: &cml::Document) -> ValidationContext { |
| ValidationContext { |
| document: &document, |
| 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)?; |
| } |
| } |
| |
| // 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)?; |
| } |
| } |
| |
| // 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 exactly one runner. |
| self.validate_runner_specified( |
| self.document.program.as_ref(), |
| self.document.r#use.as_ref(), |
| )?; |
| |
| // Validate "environments". |
| if let Some(environments) = &self.document.environments { |
| for env in environments { |
| self.validate_environment(&env, &mut strong_dependencies)?; |
| } |
| } |
| |
| // 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::Environment(environment_name.as_str()); |
| let target = DependencyNode::Child(child.name.as_str()); |
| strong_dependencies.add_edge(source, target); |
| } |
| } |
| } |
| Ok(()) |
| } |
| |
| fn validate_collection(&self, collection: &'a cml::Collection) -> Result<(), Error> { |
| 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 |
| ))); |
| } |
| // If there is an environment, we don't need to account for it in the dependency |
| // graph because a collection is always a sink node. |
| } |
| } |
| } |
| 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.runner.is_some() && capability.from.is_none() { |
| return Err(Error::validate("\"from\" should 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_clause(capability, cml::RoutingClauseType::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>, |
| ) -> Result<(), Error> { |
| if use_.service.is_some() && use_.r#as.is_some() { |
| return Err(Error::validate("\"as\" field cannot be used with \"service\"")); |
| } |
| if use_.protocol.is_some() && use_.r#as.is_some() { |
| return Err(Error::validate("\"as\" field cannot be used with \"protocol\"")); |
| } |
| if use_.directory.is_some() && use_.r#as.is_some() { |
| return Err(Error::validate("\"as\" field cannot be used with \"directory\"")); |
| } |
| if use_.runner.is_some() && use_.r#as.is_some() { |
| return Err(Error::validate("\"as\" field cannot be used with \"runner\"")); |
| } |
| if use_.event.is_some() && use_.from.is_none() { |
| return Err(Error::validate("\"from\" should be present with \"event\"")); |
| } |
| 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\" field cannot be used with \"storage\"")); |
| } |
| if use_.storage.is_some() && use_.r#as.is_some() { |
| return Err(Error::validate("\"as\" field cannot be used with \"storage\"")); |
| } |
| |
| if let Some(event_stream) = use_.event_stream.as_ref() { |
| if use_.path.is_none() { |
| return Err(Error::validate("\"path\" should be present with \"event_stream\"")); |
| } |
| let events = event_stream.to_vec(); |
| for event in events { |
| if !self.all_event_names.contains(event) { |
| return Err(Error::validate(format!( |
| "Event \"{}\" in event stream not found in any \"use\" declaration.", |
| event |
| ))); |
| } |
| } |
| } |
| |
| // Disallow multiple capability ids of the same name. |
| let capability_ids = cml::CapabilityId::from_clause(use_, cml::RoutingClauseType::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 can't be the same or partially overlap. |
| (cml::CapabilityId::UsedDirectory(_), cml::CapabilityId::UsedDirectory(_)) => { |
| dir == used_dir || dir.starts_with(used_dir) || used_dir.starts_with(dir) |
| } |
| |
| // Protocols and Services can't overlap with Directories. |
| (_, cml::CapabilityId::UsedDirectory(_)) |
| | (cml::CapabilityId::UsedDirectory(_), _) => { |
| 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)) |
| } |
| } { |
| // TODO: This error message is in the wrong order |
| return Err(Error::validate(format!( |
| "{} \"{}\" is a prefix of \"use\" target {} \"{}\"", |
| capability_id.type_str(), |
| capability_id, |
| used_id.type_str(), |
| used_id, |
| ))); |
| } |
| } |
| } |
| |
| // All directory "use" expressions must have directory rights. |
| if use_.directory.is_some() { |
| match &use_.rights { |
| Some(rights) => self.validate_directory_rights(&rights)?, |
| None => return Err(Error::validate("Rights required for this use statement.")), |
| }; |
| } |
| |
| if let Some(cml::UseFromRef::Named(name)) = &use_.from { |
| self.validate_component_capability_ref("\"use\" source", &cml::AnyRef::Named(name))?; |
| } |
| |
| 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_name) = &expose.service { |
| if expose.from.iter().any(|r| *r == cml::ExposeFromRef::Self_) { |
| if !self.all_services.contains(service_name) { |
| return Err(Error::validate(format!( |
| "Service \"{}\" is exposed from self, so it must be declared as a \"service\" in \"capabilities\"", |
| service_name |
| ))); |
| } |
| } |
| } |
| |
| // 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 |
| ))); |
| } |
| } |
| } |
| } |
| |
| // Ensure that directories exposed from self are defined in `capabilities`. |
| if let Some(directory) = &expose.directory { |
| 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_name) = &expose.runner { |
| if expose.from.iter().any(|r| *r == cml::ExposeFromRef::Self_) { |
| if !self.all_runners.contains(runner_name) { |
| return Err(Error::validate(format!( |
| "Runner \"{}\" is exposed from self, so it must be declared as a \"runner\" in \"capabilities\"", |
| runner_name |
| ))); |
| } |
| } |
| } |
| |
| // Ensure that resolvers exposed from self are defined in `capabilities`. |
| if let Some(resolver_name) = &expose.resolver { |
| if expose.from.iter().any(|r| *r == cml::ExposeFromRef::Self_) { |
| if !self.all_resolvers.contains(resolver_name) { |
| return Err(Error::validate(format!( |
| "Resolver \"{}\" is exposed from self, so it must be declared as a \"resolver\" in \"capabilities\"", resolver_name |
| ))); |
| } |
| } |
| } |
| |
| // Ensure we haven't already exposed an entity of the same name. |
| let capability_ids = |
| cml::CapabilityId::from_clause(expose, cml::RoutingClauseType::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)?; |
| |
| 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_name) = &offer.service { |
| if offer.from.iter().any(|r| *r == cml::OfferFromRef::Self_) { |
| if !self.all_services.contains(service_name) { |
| return Err(Error::validate(format!( |
| "Service \"{}\" is offered from self, so it must be declared as a \ |
| \"service\" in \"capabilities\"", |
| service_name |
| ))); |
| } |
| } |
| } |
| |
| // Ensure that protocols offered from self are defined in `capabilities`. |
| if let Some(protocol) = offer.protocol.as_ref() { |
| for protocol in protocol.iter() { |
| 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 |
| ))); |
| } |
| } |
| } |
| } |
| |
| // Ensure that directories offered from self are defined in `capabilities`. |
| if let Some(directory) = &offer.directory { |
| 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 { |
| 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_name) = &offer.runner { |
| if offer.from.iter().any(|r| *r == cml::OfferFromRef::Self_) { |
| if !self.all_runners.contains(runner_name) { |
| return Err(Error::validate(format!( |
| "Runner \"{}\" is offered from self, so it must be declared as a \ |
| \"runner\" in \"capabilities\"", |
| runner_name |
| ))); |
| } |
| } |
| } |
| |
| // Ensure that resolvers offered from self are defined in `resolvers`. |
| if let Some(resolver_name) = &offer.resolver { |
| if offer.from.iter().any(|r| *r == cml::OfferFromRef::Self_) { |
| if !self.all_resolvers.contains(resolver_name) { |
| return Err(Error::validate(format!( |
| "Resolver \"{}\" is offered from self, so it must be declared as a \ |
| \"resolver\" in \"capabilities\"", |
| resolver_name |
| ))); |
| } |
| } |
| } |
| |
| // 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.filter) { |
| (None, Some(_)) => Err(Error::validate("\"filter\" can only be used with \"event\"")), |
| _ => Ok(()), |
| }?; |
| |
| // Validate every target of this offer. |
| let target_cap_ids = cml::CapabilityId::from_clause(offer, cml::RoutingClauseType::Offer)?; |
| for to in &offer.to.0 { |
| // 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() { |
| // 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 cml::OfferFromRef::Named(from) = from { |
| match to { |
| cml::OfferToRef::Named(to) => { |
| let source = DependencyNode::Child(from.as_str()); |
| let target = DependencyNode::Child(to.as_str()); |
| strong_dependencies.add_edge(source, target); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| // Validate `from` (done last because this validation depends on the capability type, which |
| // must be validated first) |
| self.validate_from_clause("offer", offer)?; |
| |
| Ok(()) |
| } |
| |
| /// Validates that the from clause: |
| /// |
| /// - is applicable to the capability type, |
| /// - does not contain duplicates, |
| /// - references names that exist. |
| /// |
| /// `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) -> 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_name() |
| ))); |
| } |
| |
| 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). |
| if cap.protocol().is_some() { |
| self.validate_component_child_or_capability_ref( |
| &reference_description, |
| from_clause, |
| )?; |
| } else { |
| self.validate_component_child_ref(&reference_description, &from_clause)?; |
| } |
| } |
| 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 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 exactly one runner. |
| fn validate_runner_specified( |
| &self, |
| program: Option<&serde_json::map::Map<String, serde_json::value::Value>>, |
| use_: Option<&Vec<cml::Use>>, |
| ) -> Result<(), Error> { |
| // Components that have no "program" don't need a runner. |
| if program.is_none() { |
| return Ok(()); |
| } |
| |
| // Otherwise, ensure a runner is being used. |
| let mut runners_used = 0; |
| if let Some(use_) = use_ { |
| runners_used = use_.iter().filter(|u| u.runner.is_some()).count(); |
| } |
| match runners_used { |
| 0 => Err(Error::validate(concat!( |
| "Component has a 'program' block defined, but doesn't 'use' ", |
| "a runner capability. Components need to 'use' a runner ", |
| "to actually execute code." |
| ))), |
| 1 => Ok(()), |
| _ => { |
| Err(Error::validate("Component `use`s multiple runners! Must specify exactly one.")) |
| } |
| } |
| } |
| |
| 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 cml::RegistrationRef::Named(child_name) = ®istration.from { |
| let source = DependencyNode::Child(child_name.as_str()); |
| let target = DependencyNode::Environment(environment.name.as_str()); |
| strong_dependencies.add_edge(source, target); |
| } |
| } |
| } |
| |
| 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 cml::RegistrationRef::Named(child_name) = ®istration.from { |
| let source = DependencyNode::Child(child_name.as_str()); |
| let target = DependencyNode::Environment(environment.name.as_str()); |
| strong_dependencies.add_edge(source, target); |
| } |
| } |
| } |
| 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> { |
| Child(&'a str), |
| Environment(&'a str), |
| } |
| |
| impl<'a> fmt::Display for DependencyNode<'a> { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| match self { |
| DependencyNode::Child(name) => write!(f, "child {}", name), |
| DependencyNode::Environment(name) => write!(f, "environment {}", name), |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use crate::error::Location; |
| use lazy_static::lazy_static; |
| use matches::assert_matches; |
| 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_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> { |
| 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], &[]) |
| } |
| |
| #[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. |
| { "service": "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": [ |
| { |
| "protocol": "fuchsia.logger.Log", |
| "from": "self", |
| }, |
| ], |
| }"##; |
| 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 \"self\", expected \"parent\", \"framework\", \"#<capability-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": { "binary": "bin/app" }, |
| "use": [ { "runner": "elf" } ], |
| } |
| ), |
| Ok(()) |
| ), |
| |
| // use |
| test_cml_use( |
| json!({ |
| "use": [ |
| { "service": "CoolFonts", "path": "/svc/fuchsia.fonts.Provider" }, |
| { "service": "fuchsia.sys2.Realm", "from": "framework" }, |
| { "protocol": "CoolFonts", "path": "/svc/MyFonts" }, |
| { "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" }, |
| { "storage": "meta" }, |
| { "storage": "ephemral" }, |
| { "storage": "hippos", "path": "/i-love-hippos" }, |
| { "runner": "elf" }, |
| { "event": [ "started", "stopped"], "from": "parent" }, |
| { "event": [ "launched"], "from": "framework" }, |
| { "event": "destroyed", "from": "framework", "as": "destroyed_x" }, |
| { |
| "event": "capability_ready_diagnostics", |
| "as": "capability_ready", |
| "from": "parent", |
| "filter": { |
| "name": "diagnositcs" |
| } |
| }, |
| { |
| "event_stream": [ "started", "stopped", "launched" ], |
| "path": "/svc/my_stream" |
| }, |
| ], |
| "capabilities": [ |
| { |
| "storage": "data-storage", |
| "from": "parent", |
| "backing_dir": "minfs" |
| } |
| ] |
| }), |
| 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_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\"" |
| ), |
| test_cml_use_as_with_service( |
| json!({ |
| "use": [ { "service": "foo", "as": "xxx" } ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"as\" field cannot be used with \"service\"" |
| ), |
| test_cml_use_as_with_protocol( |
| json!({ |
| "use": [ { "protocol": "foo", "as": "xxx" } ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"as\" field cannot be used with \"protocol\"" |
| ), |
| test_cml_use_as_with_directory( |
| json!({ |
| "use": [ { "directory": "foo", "as": "xxx" } ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"as\" field cannot be used with \"directory\"" |
| ), |
| test_cml_use_as_with_runner( |
| json!({ |
| "use": [ { "runner": "elf", "as": "xxx" } ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"as\" field cannot be used with \"runner\"" |
| ), |
| test_cml_use_as_with_storage( |
| json!({ |
| "use": [ { "storage": "cache", "as": "mystorage" } ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"as\" field 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\" field cannot be used with \"storage\"" |
| ), |
| test_cml_use_invalid_from( |
| json!({ |
| "use": [ |
| { "service": "CoolFonts", "from": "self" } |
| ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"self\", expected \"parent\", \"framework\", \"#<capability-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 \"capabilities\"" |
| ), |
| test_cml_use_bad_path( |
| json!({ |
| "use": [ |
| { |
| "protocol": ["CoolFonts", "FunkyFonts"], |
| "path": "/MyFonts" |
| } |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"path\" field can only be specified when one `protocol` is supplied." |
| ), |
| test_cml_use_bad_duplicate_target_names( |
| json!({ |
| "use": [ |
| { "protocol": "fuchsia.sys2.Realm" }, |
| { "service": "fuchsia.sys2.Realm" }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"/svc/fuchsia.sys2.Realm\" is a duplicate \"use\" target service" |
| ), |
| 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`, `runner`, `from`, `path`, `as`, `rights`, `subdir`, `event`, `event_stream`, `filter`" |
| ), |
| |
| test_cml_use_disallows_nested_dirs( |
| 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/baz\" is a prefix of \"use\" target directory \"/foo/bar\"" |
| ), |
| test_cml_use_disallows_common_prefixes_service( |
| json!({ |
| "use": [ |
| { "directory": "foobar", "path": "/foo/bar", "rights": [ "r*" ] }, |
| { "service": "fuchsia", "path": "/foo/bar/fuchsia" }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "service \"/foo/bar/fuchsia\" is a prefix of \"use\" target directory \"/foo/bar\"" |
| ), |
| 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 == "protocol \"/foo/bar/fuchsia.2\" is a prefix of \"use\" target directory \"/foo/bar\"" |
| ), |
| 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\" field can only be specified when one `event` is supplied" |
| ), |
| 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": ["destroyed"], |
| "path": "/svc/stream", |
| }, |
| ] |
| }), |
| 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_path( |
| json!({ |
| "use": [ |
| { |
| "event": [ "destroyed" ], |
| "from": "parent", |
| }, |
| { |
| "event_stream": [ "destroyed" ], |
| }, |
| ] |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"path\" should be present with \"event_stream\"" |
| ), |
| 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\" field 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\" fields can only be specified when one `event` is supplied" |
| ), |
| |
| // expose |
| test_cml_expose( |
| json!({ |
| "expose": [ |
| { |
| "service": "fuchsia.fonts.Provider", |
| "from": "self", |
| }, |
| { |
| "service": "fuchsia.logger.Log", |
| "from": "#logger", |
| "as": "logger" |
| }, |
| { |
| "protocol": "A", |
| "from": "self", |
| }, |
| { |
| "protocol": ["B", "C"], |
| "from": "self", |
| }, |
| { |
| "protocol": "D", |
| "from": "#mystorage", |
| }, |
| { |
| "directory": "blobfs", |
| "from": "self", |
| "rights": ["r*"], |
| "subdir": "blob", |
| }, |
| { "directory": "hub", "from": "framework" }, |
| { "runner": "elf", "from": "#logger", }, |
| { "resolver": "pkg_resolver", "from": "#logger" }, |
| ], |
| "capabilities": [ |
| { "service": "fuchsia.fonts.Provider" }, |
| { "protocol": ["A", "B", "C"] }, |
| { |
| "directory": "blobfs", |
| "path": "/blobfs", |
| "rights": ["rw*"], |
| }, |
| { |
| "storage": "mystorage", |
| "from": "self", |
| "backing_dir": "blobfs" |
| } |
| ], |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm" |
| }, |
| ] |
| }), |
| 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_all_valid_chars( |
| json!({ |
| "expose": [ |
| { |
| "service": "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": [ |
| { "service": "fuchsia.logger.Log", "from": "#missing" }, |
| ], |
| }), |
| Err(Error::Validate { schema_name: None, err, .. }) if &err == "\"expose\" source \"#missing\" does not appear in \"children\"" |
| ), |
| 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": [ { |
| "service": "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 "protocol" 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\" field 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": [ |
| { |
| "service": "foo_service", |
| "from": "self", |
| }, |
| { |
| "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": [ |
| { |
| "service": "foo_service", |
| }, |
| { |
| "protocol": "foo_protocol", |
| }, |
| { |
| "protocol": "bar_protocol", |
| }, |
| { |
| "protocol": "baz_protocol", |
| }, |
| { |
| "directory": "foo_directory", |
| "path": "/dir", |
| "rights": [ "r*" ], |
| }, |
| { |
| "runner": "foo_runner", |
| "path": "/svc/runner", |
| "from": "self", |
| }, |
| { |
| "resolver": "foo_resolver", |
| "path": "/svc/resolver", |
| }, |
| ] |
| }), |
| 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_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_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": [ |
| { |
| "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" ] |
| }, |
| { |
| "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": "hub", |
| "from": "framework", |
| "to": [ "#modular" ], |
| "as": "hub", |
| "dependency": "strong" |
| }, |
| { |
| "storage": "data", |
| "from": "self", |
| "to": [ "#modular", "#logger" ] |
| }, |
| { |
| "runner": "elf", |
| "from": "parent", |
| "to": [ "#modular", "#logger" ] |
| }, |
| { |
| "resolver": "pkg_resolver", |
| "from": "parent", |
| "to": [ "#modular" ], |
| }, |
| { |
| "event": "capability_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": [ |
| { "service": "fuchsia.net.Netstack" }, |
| { |
| "directory": "assets", |
| "path": "/data/assets", |
| "rights": [ "rw*" ], |
| }, |
| { |
| "storage": "data", |
| "from": "parent", |
| "backing_dir": "minfs", |
| }, |
| ], |
| }), |
| 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_all_valid_chars( |
| json!({ |
| "offer": [ |
| { |
| "service": "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" |
| } |
| ] |
| }), |
| Ok(()) |
| ), |
| test_cml_offer_missing_props( |
| json!({ |
| "offer": [ {} ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "missing field `from`" |
| ), |
| test_cml_offer_missing_from( |
| json!({ |
| "offer": [ |
| { |
| "service": "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\"" |
| ), |
| 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": [ { |
| "service": "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_empty_targets( |
| json!({ |
| "offer": [ |
| { |
| "service": "fuchsia.logger.Log", |
| "from": "#child", |
| "to": [] |
| }, |
| ], |
| "children": [ |
| { |
| "name": "child", |
| "url": "fuchsia-pkg://", |
| }, |
| ], |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid length 0, expected a nonempty array of offer targets, with unique elements" |
| ), |
| test_cml_offer_duplicate_targets( |
| json!({ |
| "offer": [ { |
| "service": "fuchsia.logger.Log", |
| "from": "#logger", |
| "to": ["#a", "#a"] |
| } ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: array with duplicate element, expected a nonempty array of offer targets, with unique elements" |
| ), |
| test_cml_offer_target_missing_props( |
| json!({ |
| "offer": [ { |
| "service": "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": [ { |
| "service": "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": [ { |
| "service": "fuchsia.logger.Log", |
| "from": "#logger", |
| "to": [ "self" ], |
| "as": "fuchsia.logger.SysLog", |
| } ] |
| }), |
| Err(Error::Parse { err, .. }) if &err == "invalid value: string \"self\", expected \"parent\", \"framework\", \"self\", \"#<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": [ { |
| "service": "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", |
| } ], |
| }), |
| 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" |
| } ], |
| "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 "protocol" 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\" field 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": [ |
| { |
| "service": "foo_service", |
| "from": "self", |
| "to": [ "#modular" ], |
| }, |
| { |
| "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": [ |
| { |
| "service": "foo_service", |
| }, |
| { |
| "protocol": "foo_protocol", |
| }, |
| { |
| "protocol": "bar_protocol", |
| }, |
| { |
| "protocol": "baz_protocol", |
| }, |
| { |
| "directory": "foo_directory", |
| "path": "/dir", |
| "rights": [ "r*" ], |
| }, |
| { |
| "runner": "foo_runner", |
| "path": "/svc/fuchsia.sys2.ComponentRunner", |
| "from": "self", |
| }, |
| { |
| "resolver": "foo_resolver", |
| "path": "/svc/fuchsia.sys2.ComponentResolver", |
| }, |
| ] |
| }), |
| 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_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": [ { |
| "service": "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" ], |
| }, |
| { |
| "service": "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: \ |
| {{child a -> child b -> child c -> child a}, {child b -> child d -> child b}}" |
| ), |
| 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", |
| |