| // 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::{self, OneOrMany}; |
| use cm_json::{self, Error, JsonSchema, CML_SCHEMA, CMX_SCHEMA, CM_SCHEMA}; |
| use serde_json::Value; |
| use std::collections::{HashMap, HashSet}; |
| use std::fs::File; |
| use std::io::Read; |
| use std::iter; |
| use std::path::Path; |
| use 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(value: Value) -> Result<cml::Document, Error> { |
| validate_json(&value, CML_SCHEMA)?; |
| let document: cml::Document = serde_json::from_value(value) |
| .map_err(|e| Error::parse(format!("Couldn't read input as struct: {}", e)))?; |
| let mut ctx = ValidationContext { |
| document: &document, |
| all_children: HashSet::new(), |
| all_collections: HashSet::new(), |
| all_storage_and_sources: HashMap::new(), |
| }; |
| ctx.validate()?; |
| Ok(document) |
| } |
| |
| /// Read in and parse a single manifest file, and return an Error if the given file is not valid. |
| /// If the file is a .cml file and is valid, will return Some(cml::Document), and for other valid |
| /// files returns None. |
| /// |
| /// Internal single manifest file validation function, used to implement the two public validate |
| /// functions. |
| 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 \ |
| (.cm, .cml, or .cmx)"; |
| let mut buffer = String::new(); |
| File::open(&file)?.read_to_string(&mut buffer)?; |
| |
| // Validate based on file extension. |
| let ext = file.extension().and_then(|e| e.to_str()); |
| let v = match ext { |
| Some("cmx") => { |
| let v = cm_json::from_json_str(&buffer)?; |
| validate_json(&v, CMX_SCHEMA)?; |
| v |
| } |
| Some("cm") => { |
| let v = cm_json::from_json_str(&buffer)?; |
| validate_json(&v, CM_SCHEMA)?; |
| v |
| } |
| Some("cml") => { |
| let v = cm_json::from_json5_str(&buffer)?; |
| parse_cml(v.clone())?; |
| v |
| } |
| _ => { |
| return Err(Error::invalid_args(BAD_EXTENSION)); |
| } |
| }; |
| |
| // 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 }, Some(extra_msg)) => Error::Validate { |
| schema_name: schema_name.clone(), |
| err: format!("{}\n{}", err, extra_msg), |
| }, |
| _ => e, |
| })?; |
| } |
| 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, err_msgs.join(", "))); |
| } |
| Ok(()) |
| } |
| |
| struct ValidationContext<'a> { |
| document: &'a cml::Document, |
| all_children: HashSet<&'a cml::Name>, |
| all_collections: HashSet<&'a cml::Name>, |
| all_storage_and_sources: HashMap<&'a cml::Name, &'a cml::Ref>, |
| } |
| |
| /// A name/identity of a capability exposed/offered to another component. |
| /// |
| /// Exposed or offered capabilities have an identifier whose format |
| /// depends on the capability type. For directories and services this is |
| /// a path, while for storage this is a storage name. Paths and storage |
| /// names, however, are in different conceptual namespaces, and can't |
| /// collide with each other. |
| /// |
| /// This enum allows such names to be specified disambuating what |
| /// namespace they are in. |
| #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] |
| enum CapabilityId<'a> { |
| Path(&'a str), |
| Runner(&'a str), |
| StorageType(&'a str), |
| } |
| |
| impl<'a> CapabilityId<'a> { |
| /// Return the string ID of this clause. |
| pub fn as_str(&self) -> &'a str { |
| match self { |
| CapabilityId::Path(p) => p, |
| CapabilityId::Runner(r) => r, |
| CapabilityId::StorageType(s) => s, |
| } |
| } |
| |
| /// Human readable description of this capability type. |
| pub fn type_str(&self) -> &'static str { |
| match self { |
| CapabilityId::Path(_) => "path", |
| CapabilityId::Runner(_) => "runner", |
| CapabilityId::StorageType(_) => "storage type", |
| } |
| } |
| |
| /// Given a CapabilityClause (Use, Offer or Expose), return the set of target identifiers. |
| /// |
| /// When only one capability identifier is specified, the target identifier name is derived |
| /// using the "as" clause. If an "as" clause is not specified, the target identifier is the same |
| /// name as the source. |
| /// |
| /// When multiple capability identifiers are specified, the target names are the same as the |
| /// source names. |
| pub fn from_clause<'b, T>(clause: &'b T) -> Result<Vec<CapabilityId<'b>>, Error> |
| where |
| T: cml::CapabilityClause + cml::AsClause, |
| { |
| // For directory/service/runner types, return the source name, |
| // using the "as" clause to rename if neccessary. |
| let alias = clause.r#as(); |
| if let Some(svc) = clause.service().as_ref() { |
| return Ok(vec![CapabilityId::Path(alias.unwrap_or(svc))]); |
| } else if let Some(OneOrMany::One(protocol)) = clause.protocol().as_ref() { |
| return Ok(vec![CapabilityId::Path(alias.unwrap_or(protocol))]); |
| } else if let Some(OneOrMany::Many(protocols)) = clause.protocol().as_ref() { |
| return match (alias, protocols.len()) { |
| (Some(valid_alias), 1) => Ok(vec![CapabilityId::Path(valid_alias)]), |
| |
| (Some(_), _) => Err(Error::validate( |
| "\"as\" field can only be specified when one `protocol` is supplied.", |
| )), |
| |
| (None, _) => { |
| Ok(protocols.iter().map(|svc: &String| CapabilityId::Path(svc)).collect()) |
| } |
| }; |
| } else if let Some(p) = clause.directory().as_ref() { |
| return Ok(vec![CapabilityId::Path(alias.unwrap_or(p))]); |
| } else if let Some(p) = clause.runner().as_ref() { |
| return Ok(vec![CapabilityId::Runner(alias.unwrap_or(p))]); |
| } |
| |
| // Offers rules prohibit using the "as" clause for storage; this is validated outside the |
| // scope of this function. |
| if let Some(p) = clause.storage().as_ref() { |
| return Ok(vec![CapabilityId::StorageType(p)]); |
| } |
| |
| // Unknown capability type. |
| Err(Error::internal("unknown capability type")) |
| } |
| } |
| |
| impl<'a> ValidationContext<'a> { |
| 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")); |
| ensure_no_duplicates( |
| all_children_names |
| .chain(all_collection_names) |
| .chain(all_storage_names) |
| .chain(all_runner_names), |
| )?; |
| |
| // Populate the sets of children and collections. |
| self.all_children = self.document.all_children_names().into_iter().collect(); |
| self.all_collections = self.document.all_collection_names().into_iter().collect(); |
| self.all_storage_and_sources = self.document.all_storage_and_sources(); |
| |
| // Validate "use". |
| if let Some(uses) = self.document.r#use.as_ref() { |
| let mut used_ids = HashSet::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 = HashSet::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)?; |
| } |
| } |
| |
| // Validate "storage". |
| if let Some(storage) = self.document.storage.as_ref() { |
| for s in storage.iter() { |
| self.validate_component_ref("\"storage\" source", &s.from)?; |
| } |
| } |
| |
| // Validate "runners". |
| if let Some(runners) = self.document.runners.as_ref() { |
| for r in runners.iter() { |
| self.validate_component_ref("\"runner\" source", &r.from)?; |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| fn validate_use( |
| &self, |
| use_: &'a cml::Use, |
| used_ids: &mut HashSet<CapabilityId<'a>>, |
| ) -> Result<(), Error> { |
| let storage = use_.storage.as_ref().map(|s| s.as_str()); |
| match (storage, &use_.r#as) { |
| (Some("meta"), Some(_)) => { |
| Err(Error::validate("\"as\" field cannot be used with storage type \"meta\"")) |
| } |
| _ => Ok(()), |
| }?; |
| match (storage, &use_.from) { |
| (Some(_), Some(_)) => { |
| Err(Error::validate("\"from\" field cannot be used with \"storage\"")) |
| } |
| _ => Ok(()), |
| }?; |
| |
| // Disallow multiple capability ids of the same name. |
| let capability_ids = CapabilityId::from_clause(use_)?; |
| for capability_id in capability_ids { |
| if !used_ids.insert(capability_id) { |
| return Err(Error::validate(format!( |
| "\"{}\" is a duplicate \"use\" target {}", |
| capability_id.as_str(), |
| capability_id.type_str() |
| ))); |
| } |
| } |
| |
| // 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.")), |
| }; |
| } |
| |
| Ok(()) |
| } |
| |
| fn validate_expose( |
| &self, |
| expose: &'a cml::Expose, |
| used_ids: &mut HashSet<CapabilityId<'a>>, |
| ) -> Result<(), Error> { |
| self.validate_component_ref("\"expose\" source", &expose.from)?; |
| |
| // Ensure directory rights are specified if exposing from self. |
| if expose.directory.is_some() { |
| if expose.from == cml::Ref::Self_ || expose.rights.is_some() { |
| match &expose.rights { |
| Some(rights) => self.validate_directory_rights(&rights)?, |
| None => return Err(Error::validate( |
| "Rights required for this expose statement as it is exposing from self.", |
| )), |
| }; |
| } |
| } |
| |
| // Ensure we haven't already exposed an entity of the same name. |
| let capability_ids = CapabilityId::from_clause(expose)?; |
| for capability_id in capability_ids { |
| if !used_ids.insert(capability_id) { |
| return Err(Error::validate(format!( |
| "\"{}\" is a duplicate \"expose\" target {} for \"{}\"", |
| capability_id.as_str(), |
| capability_id.type_str(), |
| expose.to.as_ref().unwrap_or(&cml::Ref::Realm) |
| ))); |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| fn validate_offer( |
| &self, |
| offer: &'a cml::Offer, |
| used_ids: &mut HashMap<&'a cml::Name, HashSet<CapabilityId<'a>>>, |
| ) -> Result<(), Error> { |
| // If offered cap is a storage type, then "from" should be interpreted |
| // as a storage name. Otherwise, it should be interpreted as a child |
| // or collection. |
| if offer.storage.is_some() { |
| self.validate_storage_ref("\"offer\" source", &offer.from)?; |
| } else { |
| self.validate_component_ref("\"offer\" source", &offer.from)?; |
| } |
| |
| // Ensure directory rights are specified if offering from self. |
| if offer.directory.is_some() { |
| if offer.from == cml::Ref::Self_ || offer.rights.is_some() { |
| match &offer.rights { |
| Some(rights) => self.validate_directory_rights(&rights)?, |
| None => { |
| return Err(Error::validate( |
| "Rights required for this offer as it is offering from self.", |
| )) |
| } |
| }; |
| } |
| } |
| |
| // Validate every target of this offer. |
| for to in offer.to.iter() { |
| // Ensure the "to" value is a child. |
| let to_target = if let cml::Ref::Named(name) = to { |
| name |
| } else { |
| return Err(Error::validate(format!("invalid \"offer\" target: \"{}\"", to))); |
| }; |
| |
| // Check that any referenced child actually exists. |
| if !self.all_children.contains(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 |
| ))); |
| } |
| |
| // Storage cannot be aliased when offered. Return an error if it is used. |
| if offer.storage.is_some() && offer.r#as.is_some() { |
| return Err(Error::validate( |
| "\"as\" field cannot be used for storage offer targets", |
| )); |
| } |
| |
| // Ensure that a target is not offered more than once. |
| let target_cap_ids = CapabilityId::from_clause(offer)?; |
| let ids_for_entity = used_ids.entry(to_target).or_insert(HashSet::new()); |
| for target_cap_id in target_cap_ids { |
| if !ids_for_entity.insert(target_cap_id) { |
| return Err(Error::validate(format!( |
| "\"{}\" is a duplicate \"offer\" target {} for \"{}\"", |
| target_cap_id.as_str(), |
| target_cap_id.type_str(), |
| to |
| ))); |
| } |
| } |
| |
| // Ensure we are not offering a capability back to its source. |
| if let cml::Ref::Named(name) = &offer.from { |
| match offer.storage { |
| None => { |
| if name == to_target { |
| return Err(Error::validate(format!( |
| "Offer target \"{}\" is same as source", |
| to |
| ))); |
| } |
| } |
| Some(_) => { |
| if let Some(cml::Ref::Named(source)) = |
| self.all_storage_and_sources.get(name) |
| { |
| if to_target == source { |
| return Err(Error::validate(format!( |
| "Storage offer target \"{}\" is same as source", |
| to |
| ))); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| 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_ref( |
| &self, |
| reference_description: &str, |
| component_ref: &cml::Ref, |
| ) -> Result<(), Error> { |
| match component_ref { |
| cml::Ref::Named(name) => { |
| // Ensure we have a child defined by that name. |
| if !self.all_children.contains(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 storage reference exists. |
| /// |
| /// - `reference_description` is a human-readable description of |
| /// the reference used in error message, such as `"storage" source`. |
| /// - `storage_ref` is a reference to a storage source. |
| fn validate_storage_ref( |
| &self, |
| reference_description: &str, |
| storage_ref: &cml::Ref, |
| ) -> Result<(), Error> { |
| if let cml::Ref::Named(name) = storage_ref { |
| if !self.all_storage_and_sources.contains_key(name) { |
| return Err(Error::validate(format!( |
| "{} \"{}\" does not appear in \"storage\"", |
| reference_description, storage_ref, |
| ))); |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| /// Validates that directory rights for all route types are valid, i.e that it does not |
| /// contain duplicate rights and exists if the FromClause originates at "self". |
| /// - `keyword` is the keyword for the clause ("offer", "expose", or "use"). |
| /// - `source_obj` is the object containing the directory. |
| fn validate_directory_rights(&self, rights_clause: &Vec<String>) -> Result<(), Error> { |
| // Verify all right tokens are valid. |
| let mut rights = HashSet::new(); |
| for right_token in rights_clause.iter() { |
| match cml::parse_right_token(right_token) { |
| Some(rights_expanded) => { |
| for right in rights_expanded.into_iter() { |
| if !rights.insert(right) { |
| return Err(Error::validate(format!( |
| "\"{}\" is duplicated in the rights clause.", |
| right_token |
| ))); |
| } |
| } |
| } |
| None => { |
| return Err(Error::validate(format!( |
| "\"{}\" is not a valid right token.", |
| right_token |
| ))) |
| } |
| } |
| } |
| 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_duplicates<'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(()) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use lazy_static::lazy_static; |
| use serde_json::json; |
| use std::io::Write; |
| use tempfile::TempDir; |
| use test_util::assert_matches; |
| |
| macro_rules! test_validate_cm { |
| ( |
| $( |
| $test_name:ident => { |
| input = $input:expr, |
| result = $result:expr, |
| }, |
| )+ |
| ) => { |
| $( |
| #[test] |
| fn $test_name() { |
| validate_test("test.cm", $input, $result); |
| } |
| )+ |
| } |
| } |
| |
| macro_rules! test_validate_cml { |
| ( |
| $( |
| $test_name:ident => { |
| input = $input:expr, |
| result = $result:expr, |
| }, |
| )+ |
| ) => { |
| $( |
| #[test] |
| fn $test_name() { |
| validate_test("test.cml", $input, $result); |
| } |
| )+ |
| } |
| } |
| |
| macro_rules! test_validate_cmx { |
| ( |
| $( |
| $test_name:ident => { |
| input = $input:expr, |
| result = $result:expr, |
| }, |
| )+ |
| ) => { |
| $( |
| #[test] |
| fn $test_name() { |
| validate_test("test.cmx", $input, $result); |
| } |
| )+ |
| } |
| } |
| |
| fn validate_test( |
| filename: &str, |
| input: serde_json::value::Value, |
| expected_result: Result<(), Error>, |
| ) { |
| let input_str = format!("{}", input); |
| validate_json_str(filename, &input_str, expected_result); |
| } |
| |
| fn validate_json_str(filename: &str, input: &str, expected_result: 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.as_bytes()).unwrap(); |
| |
| let result = validate(&vec![tmp_file_path], &[]); |
| assert_eq!(format!("{:?}", result), format!("{:?}", expected_result)); |
| } |
| |
| #[test] |
| fn test_validate_invalid_json_fails() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let tmp_file_path = tmp_dir.path().join("test.cm"); |
| |
| File::create(&tmp_file_path).unwrap().write_all(b"{,}").unwrap(); |
| |
| let result = validate(&vec![tmp_file_path], &[]); |
| let expected_result: Result<(), Error> = Err(Error::parse( |
| "Couldn't read input as JSON: key must be a string at line 1 column 2", |
| )); |
| assert_eq!(format!("{:?}", result), format!("{:?}", expected_result)); |
| } |
| |
| #[test] |
| fn test_json5_parse_number() { |
| let json: Value = cm_json::from_json5_str("1").expect("couldn't parse"); |
| if let Value::Number(n) = json { |
| assert!(n.is_i64()); |
| } else { |
| panic!("{:?} is not a number", json); |
| } |
| } |
| |
| // TODO: Consider converting these tests to a golden test |
| |
| test_validate_cm! { |
| // program |
| test_cm_empty_json => { |
| input = json!({}), |
| result = Ok(()), |
| }, |
| test_cm_program => { |
| input = json!({"program": { "foo": 55 }}), |
| result = Ok(()), |
| }, |
| |
| // uses |
| test_cm_uses => { |
| input = json!({ |
| "uses": [ |
| { |
| "service": { |
| "source": { |
| "realm": {}, |
| }, |
| "source_path": "/svc/fuchsia.boot.WriteOnlyLog", |
| "target_path": "/svc/fuchsia.logger.Log" |
| } |
| }, |
| { |
| "service": { |
| "source": { |
| "framework": {}, |
| }, |
| "source_path": "/svc/fuchsia.sys2.Realm", |
| "target_path": "/svc/fuchsia.sys2.Realm" |
| } |
| }, |
| { |
| "protocol": { |
| "source": { |
| "realm": {}, |
| }, |
| "source_path": "/svc/fuchsia.boot.WriteOnlyLog", |
| "target_path": "/svc/fuchsia.logger.Log" |
| } |
| }, |
| { |
| "protocol": { |
| "source": { |
| "framework": {}, |
| }, |
| "source_path": "/svc/fuchsia.sys2.Realm", |
| "target_path": "/svc/fuchsia.sys2.Realm" |
| } |
| }, |
| { |
| "directory": { |
| "source": { |
| "realm": {}, |
| }, |
| "source_path": "/data/assets", |
| "target_path": "/data/kitten_assets" |
| } |
| }, |
| { |
| "storage": { |
| "type": "data", |
| "target_path": "/data" |
| } |
| }, |
| { |
| "storage": { |
| "type": "cache", |
| "target_path": "/data" |
| } |
| }, |
| { |
| "storage": { |
| "type": "meta" |
| } |
| } |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cm_uses_missing_variant => { |
| input = json!({ |
| "uses": [ {} ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "OneOf conditions are not met at /uses/0")), |
| }, |
| |
| // exposes |
| test_cm_exposes => { |
| input = json!({ |
| "exposes": [ |
| { |
| "service": { |
| "source_path": "/svc/fuchsia.ui.Scenic", |
| "source": { |
| "self": {} |
| }, |
| "target_path": "/svc/fuchsia.ui.Scenic", |
| "target": { |
| "realm": {}, |
| } |
| } |
| }, |
| { |
| "protocol": { |
| "source_path": "/svc/fuchsia.ui.Scenic", |
| "source": { |
| "self": {} |
| }, |
| "target_path": "/svc/fuchsia.ui.Scenic", |
| "target": { |
| "realm": {}, |
| } |
| } |
| }, |
| { |
| "directory": { |
| "source_path": "/data/assets", |
| "source": { |
| "child": { |
| "name": "cat_viewer" |
| } |
| }, |
| "target_path": "/data/kitten_assets", |
| "target": { |
| "realm": {}, |
| } |
| } |
| }, |
| { |
| "directory": { |
| "source_path": "/hub", |
| "source": { |
| "framework": {} |
| }, |
| "target_path": "/child_hub", |
| "target": { |
| "realm": {}, |
| } |
| } |
| } |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cm_exposes_missing_variant => { |
| input = json!({ |
| "exposes": [ {} ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "OneOf conditions are not met at /exposes/0")), |
| }, |
| test_cm_exposes_source_missing_variant => { |
| input = json!({ |
| "exposes": [ |
| { |
| "service": { |
| "source_path": "/svc/fuchsia.ui.Scenic", |
| "source": {}, |
| "target_path": "/svc/fuchsia.ui.Scenic", |
| "target": { |
| "realm": {}, |
| } |
| } |
| } |
| ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "OneOf conditions are not met at /exposes/0/service/source")), |
| }, |
| test_cm_exposes_source_multiple_variants => { |
| input = json!({ |
| "exposes": [ |
| { |
| "service": { |
| "source_path": "/svc/fuchsia.ui.Scenic", |
| "source": { |
| "self": {}, |
| "child": { |
| "name": "foo" |
| } |
| }, |
| "target_path": "/svc/fuchsia.ui.Scenic", |
| "target": { |
| "realm": {}, |
| } |
| } |
| } |
| ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "OneOf conditions are not met at /exposes/0/service/source")), |
| }, |
| test_cm_exposes_source_bad_child_name => { |
| input = json!({ |
| "exposes": [ |
| { |
| "service": { |
| "source_path": "/svc/fuchsia.ui.Scenic", |
| "source": { |
| "child": { |
| "name": "bad^" |
| } |
| }, |
| "target_path": "/svc/fuchsia.ui.Scenic", |
| "target": { |
| "realm": {}, |
| } |
| } |
| } |
| ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "Pattern condition is not met at /exposes/0/service/source/child/name")), |
| }, |
| |
| // offers |
| test_cm_offers => { |
| input = json!({ |
| "offers": [ |
| { |
| "service": { |
| "source": { |
| "realm": {} |
| }, |
| "source_path": "/svc/fuchsia.logger.LogSink", |
| "target": { |
| "child": { |
| "name": "viewer" |
| } |
| }, |
| "target_path": "/svc/fuchsia.logger.SysLog" |
| } |
| }, |
| { |
| "service": { |
| "source": { |
| "self": {} |
| }, |
| "source_path": "/svc/fuchsia.ui.Scenic", |
| "target": { |
| "child": { |
| "name": "user_shell" |
| } |
| }, |
| "target_path": "/svc/fuchsia.ui.Scenic" |
| } |
| }, |
| { |
| "service": { |
| "source": { |
| "self": {} |
| }, |
| "source_path": "/svc/fuchsia.ui.Scenic", |
| "target": { |
| "collection": { |
| "name": "modular" |
| } |
| }, |
| "target_path": "/services/fuchsia.ui.Scenic" |
| } |
| }, |
| { |
| "protocol": { |
| "source": { |
| "realm": {} |
| }, |
| "source_path": "/svc/fuchsia.logger.LogSink", |
| "target": { |
| "child": { |
| "name": "viewer" |
| } |
| }, |
| "target_path": "/svc/fuchsia.logger.SysLog" |
| } |
| }, |
| { |
| "protocol": { |
| "source": { |
| "self": {} |
| }, |
| "source_path": "/svc/fuchsia.ui.Scenic", |
| "target": { |
| "child": { |
| "name": "user_shell" |
| } |
| }, |
| "target_path": "/svc/fuchsia.ui.Scenic" |
| } |
| }, |
| { |
| "protocol": { |
| "source": { |
| "self": {} |
| }, |
| "source_path": "/svc/fuchsia.ui.Scenic", |
| "target": { |
| "collection": { |
| "name": "modular" |
| } |
| }, |
| "target_path": "/services/fuchsia.ui.Scenic" |
| } |
| }, |
| { |
| "directory": { |
| "source": { |
| "child": { |
| "name": "cat_provider" |
| } |
| }, |
| "source_path": "/data/assets", |
| "target": { |
| "child": { |
| "name": "cat_viewer" |
| } |
| }, |
| "target_path": "/data/kitten_assets" |
| } |
| }, |
| { |
| "directory": { |
| "source": { |
| "child": { |
| "name": "cat_provider" |
| } |
| }, |
| "source_path": "/data/assets", |
| "target": { |
| "collection": { |
| "name": "tests" |
| } |
| }, |
| "target_path": "/data/artifacts", |
| } |
| }, |
| { |
| "directory": { |
| "source": { |
| "framework": {} |
| }, |
| "source_path": "/hub", |
| "target": { |
| "collection": { |
| "name": "modular" |
| } |
| }, |
| "target_path": "/hub" |
| } |
| }, |
| { |
| "storage": { |
| "type": "data", |
| "source": { |
| "realm": {} |
| }, |
| "target": { |
| "child": { |
| "name": "cat_viewer" |
| } |
| } |
| } |
| }, |
| { |
| "storage": { |
| "type": "data", |
| "source": { |
| "realm": {} |
| }, |
| "target": { |
| "collection": { |
| "name": "tests" |
| } |
| } |
| } |
| } |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cm_offers_all_valid_chars => { |
| input = json!({ |
| "offers": [ |
| { |
| "service": { |
| "source_path": "/svc/fuchsia.logger.LogSink", |
| "source": { |
| "child": { |
| "name": "abcdefghijklmnopqrstuvwxyz0123456789_-." |
| } |
| }, |
| "target": { |
| "child": { |
| "name": "abcdefghijklmnopqrstuvwxyz0123456789_-." |
| } |
| }, |
| "target_path": "/svc/fuchsia.logger.SysLog" |
| } |
| }, |
| { |
| "storage": { |
| "type": "cache", |
| "source": { |
| "child": { |
| "name": "abcdefghijklmnopqrstuvwxyz0123456789_-." |
| } |
| }, |
| "target": { |
| "child": { |
| "name": "abcdefghijklmnopqrstuvwxyz0123456789_-." |
| } |
| } |
| } |
| } |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cm_offers_missing_variant => { |
| input = json!({ |
| "offers": [ {} ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "OneOf conditions are not met at /offers/0")), |
| }, |
| test_cm_offers_source_missing_variant => { |
| input = json!({ |
| "offers": [ |
| { |
| "service": { |
| "source_path": "/svc/fuchsia.ui.Scenic", |
| "source": {}, |
| "target": { |
| "child": { |
| "name": "user_shell" |
| } |
| }, |
| "target_path": "/svc/fuchsia.ui.Scenic" |
| } |
| }, |
| { |
| "storage": { |
| "type": "meta", |
| "source": {}, |
| "target": { |
| "child": { |
| "name": "user_shell" |
| } |
| } |
| } |
| } |
| ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "OneOf conditions are not met at /offers/0/service/source, OneOf conditions are not met at /offers/1/storage/source")), |
| }, |
| test_cm_offers_source_multiple_variants => { |
| input = json!({ |
| "offers": [ |
| { |
| "service": { |
| "source_path": "/svc/fuchsia.ui.Scenic", |
| "source": { |
| "self": {}, |
| "child": { |
| "name": "foo" |
| } |
| }, |
| "target": { |
| "child": { |
| "name": "user_shell" |
| } |
| }, |
| "target_path": "/svc/fuchsia.ui.Scenic" |
| } |
| }, |
| { |
| "storage": { |
| "type": "cache", |
| "source": { |
| "realm": {}, |
| "storage": { |
| "name": "foo" |
| } |
| }, |
| "target": { |
| "child": { |
| "name": "user_shell" |
| } |
| } |
| } |
| } |
| ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "OneOf conditions are not met at /offers/0/service/source, OneOf conditions are not met at /offers/1/storage/source")), |
| }, |
| test_cm_offers_source_bad_child_name => { |
| input = json!({ |
| "offers": [ |
| { |
| "service": { |
| "source_path": "/svc/fuchsia.ui.Scenic", |
| "source": { |
| "child": { |
| "name": "bad^" |
| } |
| }, |
| "target": { |
| "child": { |
| "name": "user_shell" |
| } |
| }, |
| "target_path": "/svc/fuchsia.ui.Scenic" |
| } |
| }, |
| { |
| "storage": { |
| "type": "data", |
| "source": { |
| "child": { |
| "name": "bad^" |
| } |
| }, |
| "target": { |
| "child": { |
| "name": "user_shell" |
| } |
| } |
| } |
| } |
| ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "Pattern condition is not met at /offers/0/service/source/child/name, Pattern condition is not met at /offers/1/storage/source/child/name")), |
| }, |
| test_cm_offers_target_missing_variant => { |
| input = json!({ |
| "offers": [ |
| { |
| "service": { |
| "source_path": "/svc/fuchsia.ui.Scenic", |
| "source": { |
| "child": { |
| "name": "cat_viewer" |
| } |
| }, |
| "target": {}, |
| "target_path": "/svc/fuchsia.ui.Scenic" |
| } |
| } |
| ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "OneOf conditions are not met at /offers/0/service/target")), |
| }, |
| test_cm_offers_target_bad_child_name => { |
| input = json!({ |
| "offers": [ |
| { |
| "service": { |
| "source_path": "/svc/fuchsia.ui.Scenic", |
| "source": { |
| "self": {} |
| }, |
| "target": { |
| "child": { |
| "name": "bad^" |
| } |
| }, |
| "target_path": "/svc/fuchsia.ui.Scenic" |
| } |
| }, |
| { |
| "storage": { |
| "type": "data", |
| "source": { |
| "realm": {} |
| }, |
| "target": { |
| "child": { |
| "name": "bad^" |
| } |
| } |
| } |
| } |
| ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "Pattern condition is not met at /offers/0/service/target/child/name, Pattern condition is not met at /offers/1/storage/target/child/name")), |
| }, |
| |
| // storage |
| test_cm_storage => { |
| input = json!({ |
| "storage": [ |
| { |
| "name": "foo", |
| "source_path": "/minfs", |
| "source": { |
| "self": {} |
| } |
| }, |
| { |
| "name": "bar", |
| "source_path": "/minfs", |
| "source": { |
| "child": { |
| "name": "minfs" |
| } |
| } |
| } |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cm_storage_missing_fields => { |
| input = json!({ |
| "storage": [ { } ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "This property is required at /storage/0/name, This property is required at /storage/0/source, This property is required at /storage/0/source_path")), |
| }, |
| test_cm_storage_invalid_child_name => { |
| input = json!({ |
| "storage": [ { |
| "name": "foo", |
| "source_path": "/minfs", |
| "source": { |
| "child": { |
| "name": "bad^" |
| } |
| } |
| } ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "Pattern condition is not met at /storage/0/source/child/name")), |
| }, |
| |
| // children |
| test_cm_children => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "system-logger2", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| "startup": "lazy" |
| }, |
| { |
| "name": "abc123_-", |
| "url": "https://www.google.com/gmail", |
| "startup": "eager" |
| } |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cm_children_missing_props => { |
| input = json!({ |
| "children": [ {} ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "This property is required at /children/0/name, This property is required at /children/0/startup, This property is required at /children/0/url")), |
| }, |
| test_cm_children_bad_name => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "bad^", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| "startup": "lazy" |
| } |
| ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "Pattern condition is not met at /children/0/name")), |
| }, |
| |
| // collections |
| test_cm_collections => { |
| input = json!({ |
| "collections": [ |
| { |
| "name": "modular", |
| "durability": "persistent" |
| }, |
| { |
| "name": "tests", |
| "durability": "transient" |
| } |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cm_collections_missing_props => { |
| input = json!({ |
| "collections": [ {} ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "This property is required at /collections/0/durability, This property is required at /collections/0/name")), |
| }, |
| test_cm_collections_bad_name => { |
| input = json!({ |
| "collections": [ |
| { |
| "name": "bad^", |
| "durability": "persistent" |
| } |
| ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "Pattern condition is not met at /collections/0/name")), |
| }, |
| |
| // runners |
| test_cm_runner => { |
| input = json!({ |
| "runners": [ |
| { |
| "name": "elf", |
| "source_path": "/elf", |
| "source": { |
| "child": { |
| "name": "child" |
| } |
| } |
| }, |
| { |
| "name": "web", |
| "source_path": "/web", |
| "source": { |
| "self": {} |
| } |
| }, |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cm_runner_missing_fields => { |
| input = json!({ |
| "runners": [ { } ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, concat!( |
| "This property is required at /runners/0/name, ", |
| "This property is required at /runners/0/source, ", |
| "This property is required at /runners/0/source_path" |
| ))), |
| }, |
| test_cm_runner_invalid_child_name => { |
| input = json!({ |
| "runners": [ { |
| "name": "foo", |
| "source_path": "/minfs", |
| "source": { |
| "child": { |
| "name": "bad^" |
| } |
| } |
| } ] |
| }), |
| result = Err(Error::validate_schema( |
| CM_SCHEMA, |
| "Pattern condition is not met at /runners/0/source/child/name", |
| )), |
| }, |
| |
| // facets |
| test_cm_facets => { |
| input = json!({ |
| "facets": { |
| "metadata": { |
| "title": "foo", |
| "authors": [ "me", "you" ], |
| "year": 2018 |
| } |
| } |
| }), |
| result = Ok(()), |
| }, |
| test_cm_facets_wrong_type => { |
| input = json!({ |
| "facets": 55 |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "Type of the value is wrong at /facets")), |
| }, |
| |
| // constraints |
| test_cm_path => { |
| input = json!({ |
| "uses": [ |
| { |
| "directory": { |
| "source": { |
| "realm": {}, |
| }, |
| "source_path": "/foo/?!@#$%/Bar", |
| "target_path": "/bar/&*()/Baz" |
| } |
| } |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cm_path_invalid_empty => { |
| input = json!({ |
| "uses": [ |
| { |
| "directory": { |
| "source": { |
| "realm": {}, |
| }, |
| "source_path": "", |
| "target_path": "/bar" |
| } |
| } |
| ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "MinLength condition is not met at /uses/0/directory/source_path, Pattern condition is not met at /uses/0/directory/source_path")), |
| }, |
| test_cm_path_invalid_root => { |
| input = json!({ |
| "uses": [ |
| { |
| "directory": { |
| "source": { |
| "realm": {}, |
| }, |
| "source_path": "/", |
| "target_path": "/bar" |
| } |
| } |
| ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "Pattern condition is not met at /uses/0/directory/source_path")), |
| }, |
| test_cm_path_invalid_relative => { |
| input = json!({ |
| "uses": [ |
| { |
| "directory": { |
| "source": { |
| "realm": {}, |
| }, |
| "source_path": "foo/bar", |
| "target_path": "/bar" |
| } |
| } |
| ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "Pattern condition is not met at /uses/0/directory/source_path")), |
| }, |
| test_cm_path_invalid_trailing => { |
| input = json!({ |
| "uses": [ |
| { |
| "directory": { |
| "source": { |
| "realm": {}, |
| }, |
| "source_path": "/foo/bar/", |
| "target_path": "/bar" |
| } |
| } |
| ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "Pattern condition is not met at /uses/0/directory/source_path")), |
| }, |
| test_cm_path_too_long => { |
| input = json!({ |
| "uses": [ |
| { |
| "directory": { |
| "source": { |
| "realm": {}, |
| }, |
| "source_path": format!("/{}", "a".repeat(1024)), |
| "target_path": "/bar" |
| } |
| } |
| ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "MaxLength condition is not met at /uses/0/directory/source_path")), |
| }, |
| test_cm_name => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "abcdefghijklmnopqrstuvwxyz0123456789_-.", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| "startup": "lazy" |
| } |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cm_name_invalid => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "#bad", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| "startup": "lazy" |
| } |
| ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "Pattern condition is not met at /children/0/name")), |
| }, |
| test_cm_name_too_long => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "a".repeat(101), |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| "startup": "lazy" |
| } |
| ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "MaxLength condition is not met at /children/0/name")), |
| }, |
| test_cm_url => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "my+awesome-scheme.2://abc123!@#$%.com", |
| "startup": "lazy" |
| } |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cm_url_invalid => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://", |
| "startup": "lazy" |
| } |
| ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "Pattern condition is not met at /children/0/url")), |
| }, |
| test_cm_url_too_long => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": &format!("fuchsia-pkg://{}", "a".repeat(4083)), |
| "startup": "lazy" |
| } |
| ] |
| }), |
| result = Err(Error::validate_schema(CM_SCHEMA, "MaxLength condition is not met at /children/0/url")), |
| }, |
| } |
| |
| #[test] |
| fn test_cml_json5() { |
| let input = r##"{ |
| "expose": [ |
| // Here are some services to expose. |
| { "service": "/loggers/fuchsia.logger.Log", "from": "#logger", }, |
| { "directory": "/volumes/blobfs", "from": "self", "rights": ["rw*"]}, |
| ], |
| "children": [ |
| { |
| 'name': 'logger', |
| 'url': 'fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm', |
| }, |
| ], |
| }"##; |
| validate_json_str("test.cml", input, Ok(())); |
| } |
| |
| test_validate_cml! { |
| // program |
| test_cml_empty_json => { |
| input = json!({}), |
| result = Ok(()), |
| }, |
| test_cml_program => { |
| input = json!({"program": { "binary": "bin/app" }}), |
| result = Ok(()), |
| }, |
| test_cml_program_no_binary => { |
| input = json!({"program": {}}), |
| result = Err(Error::validate_schema(CML_SCHEMA, "This property is required at /program/binary")), |
| }, |
| |
| // use |
| test_cml_use => { |
| input = json!({ |
| "use": [ |
| { "service": "/fonts/CoolFonts", "as": "/svc/fuchsia.fonts.Provider" }, |
| { "service": "/svc/fuchsia.sys2.Realm", "from": "framework" }, |
| { "protocol": "/fonts/CoolFonts", "as": "/svc/MyFonts" }, |
| { "protocol": "/svc/fuchsia.test.hub.HubReport", "from": "framework" }, |
| { "protocol": ["/svc/fuchsia.ui.scenic.Scenic", "/svc/fuchsia.net.Connectivity"] }, |
| { |
| "directory": "/data/assets", |
| "rights": ["rw*"], |
| }, |
| { |
| "directory": "/data/config", |
| "from": "realm", |
| "rights": ["rx*"], |
| }, |
| { "storage": "data", "as": "/example" }, |
| { "storage": "cache", "as": "/tmp" }, |
| { "storage": "meta" }, |
| { "runner": "elf" } |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cml_use_missing_props => { |
| input = json!({ |
| "use": [ { "as": "/svc/fuchsia.logger.Log" } ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "OneOf conditions are not met at /use/0")), |
| }, |
| test_cml_use_as_with_meta_storage => { |
| input = json!({ |
| "use": [ { "storage": "meta", "as": "/meta" } ] |
| }), |
| result = Err(Error::validate("\"as\" field cannot be used with storage type \"meta\"")), |
| }, |
| test_cml_use_as_with_runner => { |
| input = json!({ |
| "use": [ { "runner": "elf", "as": "xxx" } ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "Pattern condition is not met at /use/0/as")), |
| }, |
| test_cml_use_from_with_meta_storage => { |
| input = json!({ |
| "use": [ { "storage": "cache", "from": "realm" } ] |
| }), |
| result = Err(Error::validate("\"from\" field cannot be used with \"storage\"")), |
| }, |
| test_cml_use_invalid_from => { |
| input = json!({ |
| "use": [ |
| { "service": "/fonts/CoolFonts", "from": "self" } |
| ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "Pattern condition is not met at /use/0/from")), |
| }, |
| test_cml_use_bad_as => { |
| input = json!({ |
| "use": [ |
| { |
| "protocol": ["/fonts/CoolFonts", "/fonts/FunkyFonts"], |
| "as": "/fonts/MyFonts" |
| } |
| ] |
| }), |
| result = Err(Error::validate("\"as\" field can only be specified when one `protocol` is supplied.")), |
| }, |
| test_cml_use_bad_duplicate_targets => { |
| input = json!({ |
| "use": [ |
| { "service": "/svc/fuchsia.sys2.Realm", "from": "framework" }, |
| { "protocol": "/svc/fuchsia.sys2.Realm", "from": "framework" }, |
| ], |
| }), |
| result = Err(Error::validate("\"/svc/fuchsia.sys2.Realm\" is a duplicate \"use\" target path")), |
| }, |
| test_cml_use_bad_duplicate_protocol => { |
| input = json!({ |
| "use": [ |
| { "protocol": ["/svc/fuchsia.sys2.Realm", "/svc/fuchsia.sys2.Realm"] }, |
| ], |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "OneOf conditions are not met at /use/0/protocol")), |
| }, |
| test_cml_use_empty_protocols => { |
| input = json!({ |
| "use": [ |
| { |
| "protocol": [], |
| }, |
| ], |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "OneOf conditions are not met at /use/0/protocol")), |
| }, |
| |
| // expose |
| test_cml_expose => { |
| input = json!({ |
| "expose": [ |
| { |
| "service": "/loggers/fuchsia.logger.Log", |
| "from": "#logger", |
| "as": "/svc/logger" |
| }, |
| { |
| "protocol": "/svc/A", |
| "from": "self", |
| }, |
| { |
| "protocol": ["/svc/B", "/svc/C"], |
| "from": "self", |
| }, |
| { "directory": "/volumes/blobfs", "from": "self", "rights": ["r*"]}, |
| { "directory": "/hub", "from": "framework" }, |
| { "runner": "elf", "from": "#logger", } |
| ], |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm" |
| } |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cml_expose_all_valid_chars => { |
| input = json!({ |
| "expose": [ |
| { "service": "/loggers/fuchsia.logger.Log", "from": "#abcdefghijklmnopqrstuvwxyz0123456789_-." } |
| ], |
| "children": [ |
| { |
| "name": "abcdefghijklmnopqrstuvwxyz0123456789_-.", |
| "url": "https://www.google.com/gmail" |
| } |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cml_expose_missing_props => { |
| input = json!({ |
| "expose": [ {} ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "OneOf conditions are not met at /expose/0, This property is required at /expose/0/from")), |
| }, |
| test_cml_expose_missing_from => { |
| input = json!({ |
| "expose": [ |
| { "service": "/loggers/fuchsia.logger.Log", "from": "#missing" } |
| ] |
| }), |
| result = Err(Error::validate("\"expose\" source \"#missing\" does not appear in \"children\"")), |
| }, |
| test_cml_expose_duplicate_target_paths => { |
| input = json!({ |
| "expose": [ |
| { "service": "/fonts/CoolFonts", "from": "self" }, |
| { "service": "/svc/logger", "from": "#logger", "as": "/thing" }, |
| { "directory": "/thing", "from": "self" , "rights": ["rx*"] } |
| ], |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm" |
| } |
| ] |
| }), |
| result = Err(Error::validate( |
| "\"/thing\" is a duplicate \"expose\" target path for \"realm\"" |
| )), |
| }, |
| test_cml_expose_bad_from => { |
| input = json!({ |
| "expose": [ { |
| "service": "/loggers/fuchsia.logger.Log", "from": "realm" |
| } ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "Pattern condition is not met at /expose/0/from")), |
| }, |
| // if "as" is specified, only 1 "protocol" array item is allowed. |
| test_cml_expose_bad_as => { |
| input = json!({ |
| "expose": [ |
| { |
| "protocol": ["/svc/A", "/svc/B"], |
| "from": "self", |
| "as": "/thing" |
| }, |
| ], |
| "children": [ |
| { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm" |
| } |
| ] |
| }), |
| result = Err(Error::validate("\"as\" field can only be specified when one `protocol` is supplied.")), |
| }, |
| test_cml_expose_bad_duplicate_targets => { |
| input = json!({ |
| "expose": [ |
| { |
| "protocol": ["/svc/A", "/svc/B"], |
| "from": "self" |
| }, |
| { |
| "protocol": "/svc/A", |
| "from": "self" |
| }, |
| ], |
| }), |
| result = Err(Error::validate("\"/svc/A\" is a duplicate \"expose\" target path for \"realm\"")), |
| }, |
| test_cml_expose_empty_protocols => { |
| input = json!({ |
| "expose": [ |
| { |
| "protocol": [], |
| "from": "self", |
| "as": "/thing" |
| } |
| ], |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "OneOf conditions are not met at /expose/0/protocol")), |
| }, |
| |
| // offer |
| test_cml_offer => { |
| input = json!({ |
| "offer": [ |
| { |
| "service": "/svc/fuchsia.logger.Log", |
| "from": "#logger", |
| "to": [ "#echo_server", "#modular" ], |
| "as": "/svc/fuchsia.logger.SysLog" |
| }, |
| { |
| "service": "/svc/fuchsia.fonts.Provider", |
| "from": "realm", |
| "to": [ "#echo_server" ] |
| }, |
| { |
| "protocol": "/svc/fuchsia.fonts.LegacyProvider", |
| "from": "realm", |
| "to": [ "#echo_server" ] |
| }, |
| { |
| "protocol": [ |
| "/svc/fuchsia.settings.Accessibility", |
| "/svc/fuchsia.ui.scenic.Scenic" |
| ], |
| "from": "realm", |
| "to": [ "#echo_server" ] |
| }, |
| { |
| "directory": "/data/assets", |
| "from": "self", |
| "to": [ "#echo_server" ], |
| "rights": ["rw*"] |
| }, |
| { |
| "directory": "/data/index", |
| "from": "realm", |
| "to": [ "#modular" ] |
| }, |
| { |
| "directory": "/hub", |
| "from": "framework", |
| "to": [ "#modular" ], |
| "as": "/hub" |
| }, |
| { |
| "storage": "data", |
| "from": "#minfs", |
| "to": [ "#modular", "#logger" ] |
| }, |
| { |
| "runner": "elf", |
| "from": "realm", |
| "to": [ "#modular", "#logger" ] |
| } |
| ], |
| "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", |
| }, |
| ], |
| "storage": [ |
| { |
| "name": "minfs", |
| "from": "realm", |
| "path": "/minfs", |
| } |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cml_offer_all_valid_chars => { |
| input = json!({ |
| "offer": [ |
| { |
| "service": "/svc/fuchsia.logger.Log", |
| "from": "#abcdefghijklmnopqrstuvwxyz0123456789_-from", |
| "to": [ "#abcdefghijklmnopqrstuvwxyz0123456789_-to" ], |
| }, |
| { |
| "storage": "data", |
| "from": "#abcdefghijklmnopqrstuvwxyz0123456789_-storage", |
| "to": [ "#abcdefghijklmnopqrstuvwxyz0123456789_-to" ], |
| } |
| ], |
| "children": [ |
| { |
| "name": "abcdefghijklmnopqrstuvwxyz0123456789_-from", |
| "url": "https://www.google.com/gmail" |
| }, |
| { |
| "name": "abcdefghijklmnopqrstuvwxyz0123456789_-to", |
| "url": "https://www.google.com/gmail" |
| }, |
| ], |
| "storage": [ |
| { |
| "name": "abcdefghijklmnopqrstuvwxyz0123456789_-storage", |
| "from": "#abcdefghijklmnopqrstuvwxyz0123456789_-from", |
| "path": "/example" |
| } |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cml_offer_missing_props => { |
| input = json!({ |
| "offer": [ {} ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "OneOf conditions are not met at /offer/0, This property is required at /offer/0/from, This property is required at /offer/0/to")), |
| }, |
| test_cml_offer_missing_from => { |
| input = json!({ |
| "offer": [ { |
| "service": "/svc/fuchsia.logger.Log", |
| "from": "#missing", |
| "to": [ "#echo_server" ], |
| } ] |
| }), |
| result = Err(Error::validate("\"offer\" source \"#missing\" does not appear in \"children\"")), |
| }, |
| test_cml_storage_offer_missing_from => { |
| input = json!({ |
| "offer": [ { |
| "storage": "cache", |
| "from": "#missing", |
| "to": [ "#echo_server" ], |
| } ] |
| }), |
| result = Err(Error::validate("\"offer\" source \"#missing\" does not appear in \"storage\"")), |
| }, |
| test_cml_offer_bad_from => { |
| input = json!({ |
| "offer": [ { |
| "service": "/svc/fuchsia.logger.Log", |
| "from": "#invalid@", |
| "to": [ "#echo_server" ], |
| } ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "Pattern condition is not met at /offer/0/from")), |
| }, |
| test_cml_storage_offer_bad_to => { |
| input = json!({ |
| "offer": [ { |
| "storage": "cache", |
| "from": "realm", |
| "to": [ "#logger" ], |
| "as": "/invalid", |
| } ], |
| "children": [ { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger#meta/logger.cm" |
| } ] |
| }), |
| result = Err(Error::validate("\"as\" field cannot be used for storage offer targets")), |
| }, |
| test_cml_offer_empty_targets => { |
| input = json!({ |
| "offer": [ { |
| "service": "/svc/fuchsia.logger.Log", |
| "from": "#logger", |
| "to": [] |
| } ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "MinItems condition is not met at /offer/0/to")), |
| }, |
| test_cml_offer_duplicate_targets => { |
| input = json!({ |
| "offer": [ { |
| "service": "/svc/fuchsia.logger.Log", |
| "from": "#logger", |
| "to": ["#a", "#a"] |
| } ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "UniqueItems condition is not met at /offer/0/to")), |
| }, |
| test_cml_offer_target_missing_props => { |
| input = json!({ |
| "offer": [ { |
| "service": "/svc/fuchsia.logger.Log", |
| "from": "#logger", |
| "as": "/svc/fuchsia.logger.SysLog", |
| } ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "This property is required at /offer/0/to")), |
| }, |
| test_cml_offer_target_missing_to => { |
| input = json!({ |
| "offer": [ { |
| "service": "/snvc/fuchsia.logger.Log", |
| "from": "#logger", |
| "to": [ "#missing" ], |
| } ], |
| "children": [ { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm" |
| } ] |
| }), |
| result = Err(Error::validate("\"#missing\" is an \"offer\" target but it does not appear in \"children\" or \"collections\"")), |
| }, |
| test_cml_offer_target_bad_to => { |
| input = json!({ |
| "offer": [ { |
| "service": "/svc/fuchsia.logger.Log", |
| "from": "#logger", |
| "to": [ "self" ], |
| "as": "/svc/fuchsia.logger.SysLog", |
| } ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "Pattern condition is not met at /offer/0/to/0")), |
| }, |
| test_cml_offer_empty_protocols => { |
| input = json!({ |
| "offer": [ |
| { |
| "protocol": [], |
| "from": "self", |
| "to": [ "#echo_server" ], |
| "as": "/thing" |
| }, |
| ], |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "OneOf conditions are not met at /offer/0/protocol")), |
| }, |
| test_cml_offer_target_equals_from => { |
| input = json!({ |
| "offer": [ { |
| "service": "/svc/fuchsia.logger.Log", |
| "from": "#logger", |
| "to": [ "#logger" ], |
| "as": "/svc/fuchsia.logger.SysLog", |
| } ], |
| "children": [ { |
| "name": "logger", "url": "fuchsia-pkg://fuchsia.com/logger#meta/logger.cm", |
| } ], |
| }), |
| result = Err(Error::validate("Offer target \"#logger\" is same as source")), |
| }, |
| test_cml_storage_offer_target_equals_from => { |
| input = json!({ |
| "offer": [ { |
| "storage": "data", |
| "from": "#minfs", |
| "to": [ "#logger" ], |
| } ], |
| "children": [ { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger#meta/logger.cm", |
| } ], |
| "storage": [ { |
| "name": "minfs", |
| "from": "#logger", |
| "path": "/minfs", |
| } ], |
| }), |
| result = Err(Error::validate("Storage offer target \"#logger\" is same as source")), |
| }, |
| test_cml_offer_duplicate_target_paths => { |
| input = json!({ |
| "offer": [ |
| { |
| "service": "/svc/logger", |
| "from": "self", |
| "to": [ "#echo_server" ], |
| "as": "/thing" |
| }, |
| { |
| "service": "/svc/logger", |
| "from": "self", |
| "to": [ "#scenic" ], |
| }, |
| { |
| "directory": "/thing", |
| "from": "realm", |
| "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" |
| } |
| ] |
| }), |
| result = Err(Error::validate("\"/thing\" is a duplicate \"offer\" target path for \"#echo_server\"")), |
| }, |
| test_cml_offer_duplicate_storage_types => { |
| input = json!({ |
| "offer": [ |
| { |
| "storage": "cache", |
| "from": "realm", |
| "to": [ "#echo_server" ] |
| }, |
| { |
| "storage": "cache", |
| "from": "#minfs", |
| "to": [ "#echo_server" ] |
| } |
| ], |
| "storage": [ { |
| "name": "minfs", |
| "from": "self", |
| "path": "/minfs" |
| } ], |
| "children": [ { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm" |
| } ] |
| }), |
| result = Err(Error::validate("\"cache\" is a duplicate \"offer\" target storage type for \"#echo_server\"")), |
| }, |
| test_cml_offer_duplicate_runner_name => { |
| input = json!({ |
| "offer": [ |
| { |
| "runner": "elf", |
| "from": "realm", |
| "to": [ "#echo_server" ] |
| }, |
| { |
| "runner": "elf", |
| "from": "framework", |
| "to": [ "#echo_server" ] |
| } |
| ], |
| "children": [ { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm" |
| } ] |
| }), |
| result = Err(Error::validate("\"elf\" is a duplicate \"offer\" target runner for \"#echo_server\"")), |
| }, |
| // if "as" is specified, only 1 "protocol" array item is allowed. |
| test_cml_offer_bad_as => { |
| input = json!({ |
| "offer": [ |
| { |
| "protocol": ["/svc/A", "/svc/B"], |
| "from": "self", |
| "to": [ "#echo_server" ], |
| "as": "/thing" |
| }, |
| ], |
| "children": [ |
| { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm" |
| } |
| ] |
| }), |
| result = Err(Error::validate("\"as\" field can only be specified when one `protocol` is supplied.")), |
| }, |
| |
| // children |
| test_cml_children => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| }, |
| { |
| "name": "gmail", |
| "url": "https://www.google.com/gmail", |
| "startup": "eager", |
| }, |
| { |
| "name": "echo", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo.cm", |
| "startup": "lazy", |
| }, |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cml_children_missing_props => { |
| input = json!({ |
| "children": [ {} ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "This property is required at /children/0/name, This property is required at /children/0/url")), |
| }, |
| test_cml_children_duplicate_names => { |
| input = 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" |
| } |
| ] |
| }), |
| result = Err(Error::validate("identifier \"logger\" is defined twice, once in \"children\" and once in \"children\"")), |
| }, |
| test_cml_children_bad_startup => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| "startup": "zzz", |
| }, |
| ], |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "Pattern condition is not met at /children/0/startup")), |
| }, |
| |
| // collections |
| test_cml_collections => { |
| input = json!({ |
| "collections": [ |
| { |
| "name": "modular", |
| "durability": "persistent" |
| }, |
| { |
| "name": "tests", |
| "durability": "transient" |
| }, |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cml_collections_missing_props => { |
| input = json!({ |
| "collections": [ {} ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "This property is required at /collections/0/durability, This property is required at /collections/0/name")), |
| }, |
| test_cml_collections_duplicate_names => { |
| input = json!({ |
| "collections": [ |
| { |
| "name": "modular", |
| "durability": "persistent" |
| }, |
| { |
| "name": "modular", |
| "durability": "transient" |
| } |
| ] |
| }), |
| result = Err(Error::validate("identifier \"modular\" is defined twice, once in \"collections\" and once in \"collections\"")), |
| }, |
| test_cml_collections_bad_durability => { |
| input = json!({ |
| "collections": [ |
| { |
| "name": "modular", |
| "durability": "zzz", |
| }, |
| ], |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "Pattern condition is not met at /collections/0/durability")), |
| }, |
| |
| // storage |
| test_cml_storage => { |
| input = json!({ |
| "storage": [ |
| { |
| "name": "a", |
| "from": "#minfs", |
| "path": "/minfs" |
| }, |
| { |
| "name": "b", |
| "from": "realm", |
| "path": "/data" |
| }, |
| { |
| "name": "c", |
| "from": "self", |
| "path": "/storage" |
| } |
| ], |
| "children": [ |
| { |
| "name": "minfs", |
| "url": "fuchsia-pkg://fuchsia.com/minfs/stable#meta/minfs.cm" |
| } |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cml_storage_all_valid_chars => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "abcdefghijklmnopqrstuvwxyz0123456789_-from", |
| "url": "https://www.google.com/gmail" |
| }, |
| ], |
| "storage": [ |
| { |
| "name": "abcdefghijklmnopqrstuvwxyz0123456789_-storage", |
| "from": "#abcdefghijklmnopqrstuvwxyz0123456789_-from", |
| "path": "/example" |
| } |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cml_storage_missing_props => { |
| input = json!({ |
| "storage": [ {} ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "This property is required at /storage/0/from, This property is required at /storage/0/name, This property is required at /storage/0/path")), |
| }, |
| test_cml_storage_missing_from => { |
| input = json!({ |
| "storage": [ { |
| "name": "minfs", |
| "from": "#missing", |
| "path": "/minfs" |
| } ] |
| }), |
| result = Err(Error::validate("\"storage\" source \"#missing\" does not appear in \"children\"")), |
| }, |
| |
| // runner |
| test_cml_runner => { |
| input = json!({ |
| "runner": [ |
| { |
| "name": "a", |
| "from": "#minfs", |
| "path": "/minfs" |
| }, |
| { |
| "name": "b", |
| "from": "realm", |
| "path": "/data" |
| }, |
| { |
| "name": "c", |
| "from": "self", |
| "path": "/runner" |
| } |
| ], |
| "children": [ |
| { |
| "name": "minfs", |
| "url": "fuchsia-pkg://fuchsia.com/minfs/stable#meta/minfs.cm" |
| } |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cml_runner_all_valid_chars => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "abcdefghijklmnopqrstuvwxyz0123456789_-from", |
| "url": "https://www.google.com/gmail" |
| }, |
| ], |
| "runner": [ |
| { |
| "name": "abcdefghijklmnopqrstuvwxyz0123456789_-runner", |
| "from": "#abcdefghijklmnopqrstuvwxyz0123456789_-from", |
| "path": "/example" |
| } |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cml_runner_missing_props => { |
| input = json!({ |
| "runners": [ {} ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, concat!( |
| "This property is required at /runners/0/from, ", |
| "This property is required at /runners/0/name, ", |
| "This property is required at /runners/0/path", |
| ))), |
| }, |
| test_cml_runner_missing_from => { |
| input = json!({ |
| "runners": [ { |
| "name": "minfs", |
| "from": "#missing", |
| "path": "/minfs" |
| } ] |
| }), |
| result = Err(Error::validate("\"runner\" source \"#missing\" does not appear in \"children\"")), |
| }, |
| |
| // facets |
| test_cml_facets => { |
| input = json!({ |
| "facets": { |
| "metadata": { |
| "title": "foo", |
| "authors": [ "me", "you" ], |
| "year": 2018 |
| } |
| } |
| }), |
| result = Ok(()), |
| }, |
| test_cml_facets_wrong_type => { |
| input = json!({ |
| "facets": 55 |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "Type of the value is wrong at /facets")), |
| }, |
| |
| // constraints |
| test_cml_rights_all => { |
| input = json!({ |
| "use": [ |
| { |
| "directory": "/foo/bar", |
| "rights": ["connect", "enumerate", "read_bytes", "write_bytes", |
| "execute", "update_attributes", "get_attributes", "traverse", |
| "modify_directory", "admin"], |
| }, |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cml_rights_invalid => { |
| input = json!({ |
| "use": [ |
| { |
| "directory": "/foo/bar", |
| "rights": ["cAnnect", "enumerate"], |
| }, |
| ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "Enum conditions are not met at /use/0/rights/0")), |
| }, |
| test_cml_rights_duplicate => { |
| input = json!({ |
| "use": [ |
| { |
| "directory": "/foo/bar", |
| "rights": ["connect", "connect"], |
| }, |
| ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "UniqueItems condition is not met at /use/0/rights")), |
| }, |
| test_cml_rights_empty => { |
| input = json!({ |
| "use": [ |
| { |
| "directory": "/foo/bar", |
| "rights": [], |
| }, |
| ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "MinItems condition is not met at /use/0/rights")), |
| }, |
| test_cml_rights_alias_star_expansion => { |
| input = json!({ |
| "use": [ |
| { |
| "directory": "/foo/bar", |
| "rights": ["r*"], |
| }, |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cml_rights_alias_star_expansion_with_longform => { |
| input = json!({ |
| "use": [ |
| { |
| "directory": "/foo/bar", |
| "rights": ["w*", "read_bytes"], |
| }, |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cml_rights_alias_star_expansion_with_longform_collision => { |
| input = json!({ |
| "use": [ |
| { |
| "directory": "/foo/bar", |
| "rights": ["r*", "read_bytes"], |
| }, |
| ] |
| }), |
| result = Err(Error::validate("\"read_bytes\" is duplicated in the rights clause.")), |
| }, |
| test_cml_rights_alias_star_expansion_collision => { |
| input = json!({ |
| "use": [ |
| { |
| "directory": "/foo/bar", |
| "rights": ["w*", "x*"], |
| }, |
| ] |
| }), |
| result = Err(Error::validate("\"x*\" is duplicated in the rights clause.")), |
| }, |
| test_cml_rights_use_invalid => { |
| input = json!({ |
| "use": [ |
| { "directory": "/foo", }, |
| ] |
| }), |
| result = Err(Error::validate("Rights required for this use statement.")), |
| }, |
| test_cml_rights_offer_dir_invalid => { |
| input = json!({ |
| "offer": [ |
| { |
| "directory": "/foo", |
| "from": "self", |
| "to": [ "#echo_server" ], |
| }, |
| ], |
| "children": [ |
| { |
| "name": "echo_server", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm" |
| } |
| ], |
| }), |
| result = Err(Error::validate("Rights required for this offer as it is offering from self.")), |
| }, |
| test_cml_rights_expose_dir_invalid => { |
| input = json!({ |
| "expose": [ |
| { |
| "directory": "/foo/bar", |
| "from": "self", |
| }, |
| ] |
| }), |
| result = Err(Error::validate("Rights required for this expose statement as it is exposing from self.")), |
| }, |
| test_cml_path => { |
| input = json!({ |
| "use": [ |
| { |
| "directory": "/foo/?!@#$%/Bar", |
| "rights": ["read_bytes"], |
| }, |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cml_path_invalid_empty => { |
| input = json!({ |
| "use": [ |
| { "service": "" }, |
| ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "MinLength condition is not met at /use/0/service, Pattern condition is not met at /use/0/service")), |
| }, |
| test_cml_path_invalid_root => { |
| input = json!({ |
| "use": [ |
| { "service": "/" }, |
| ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "Pattern condition is not met at /use/0/service")), |
| }, |
| test_cml_path_invalid_relative => { |
| input = json!({ |
| "use": [ |
| { "service": "foo/bar" }, |
| ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "Pattern condition is not met at /use/0/service")), |
| }, |
| test_cml_path_invalid_trailing => { |
| input = json!({ |
| "use": [ |
| { "service": "/foo/bar/" }, |
| ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "Pattern condition is not met at /use/0/service")), |
| }, |
| test_cml_path_too_long => { |
| input = json!({ |
| "use": [ |
| { "service": format!("/{}", "a".repeat(1024)) }, |
| ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "MaxLength condition is not met at /use/0/service")), |
| }, |
| test_cml_relative_ref_too_long => { |
| input = json!({ |
| "expose": [ |
| { |
| "service": "/loggers/fuchsia.logger.Log", |
| "from": &format!("#{}", "a".repeat(101)), |
| }, |
| ], |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| }, |
| ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "MaxLength condition is not met at /expose/0/from")), |
| }, |
| test_cml_child_name => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "abcdefghijklmnopqrstuvwxyz0123456789_-.", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| }, |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cml_child_name_invalid => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "#bad", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| }, |
| ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "Pattern condition is not met at /children/0/name")), |
| }, |
| test_cml_child_name_too_long => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "a".repeat(101), |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| } |
| ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "MaxLength condition is not met at /children/0/name")), |
| }, |
| test_cml_url => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "my+awesome-scheme.2://abc123!@#$%.com", |
| }, |
| ] |
| }), |
| result = Ok(()), |
| }, |
| test_cml_url_invalid => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://", |
| }, |
| ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "Pattern condition is not met at /children/0/url")), |
| }, |
| test_cml_url_too_long => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": &format!("fuchsia-pkg://{}", "a".repeat(4083)), |
| }, |
| ] |
| }), |
| result = Err(Error::validate_schema(CML_SCHEMA, "MaxLength condition is not met at /children/0/url")), |
| }, |
| test_cml_duplicate_identifiers_children_collection => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm" |
| } |
| ], |
| "collections": [ |
| { |
| "name": "logger", |
| "durability": "transient" |
| } |
| ] |
| }), |
| result = Err(Error::validate("identifier \"logger\" is defined twice, once in \"collections\" and once in \"children\"")), |
| }, |
| test_cml_duplicate_identifiers_children_storage => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm" |
| } |
| ], |
| "storage": [ |
| { |
| "name": "logger", |
| "path": "/logs", |
| "from": "realm" |
| } |
| ] |
| }), |
| result = Err(Error::validate("identifier \"logger\" is defined twice, once in \"storage\" and once in \"children\"")), |
| }, |
| test_cml_duplicate_identifiers_collection_storage => { |
| input = json!({ |
| "collections": [ |
| { |
| "name": "logger", |
| "durability": "transient" |
| } |
| ], |
| "storage": [ |
| { |
| "name": "logger", |
| "path": "/logs", |
| "from": "realm" |
| } |
| ] |
| }), |
| result = Err(Error::validate("identifier \"logger\" is defined twice, once in \"storage\" and once in \"collections\"")), |
| }, |
| test_cml_duplicate_identifiers_children_runners => { |
| input = json!({ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm" |
| } |
| ], |
| "runners": [ |
| { |
| "name": "logger", |
| "path": "/logs", |
| "from": "realm" |
| } |
| ] |
| }), |
| result = Err(Error::validate("identifier \"logger\" is defined twice, once in \"runners\" and once in \"children\"")), |
| }, |
| } |
| |
| test_validate_cmx! { |
| test_cmx_err_empty_json => { |
| input = json!({}), |
| result = Err(Error::validate_schema(CMX_SCHEMA, "This property is required at /program")), |
| }, |
| test_cmx_program => { |
| input = json!({"program": { "binary": "bin/app" }}), |
| result = Ok(()), |
| }, |
| test_cmx_program_no_binary => { |
| input = json!({ "program": {}}), |
| result = Err(Error::validate_schema(CMX_SCHEMA, "OneOf conditions are not met at /program")), |
| }, |
| test_cmx_bad_program => { |
| input = json!({"prigram": { "binary": "bin/app" }}), |
| result = Err(Error::validate_schema(CMX_SCHEMA, "Property conditions are not met at , \ |
| This property is required at /program")), |
| }, |
| test_cmx_sandbox => { |
| input = json!({ |
| "program": { "binary": "bin/app" }, |
| "sandbox": { "dev": [ "class/camera" ] } |
| }), |
| result = Ok(()), |
| }, |
| test_cmx_facets => { |
| input = json!({ |
| "program": { "binary": "bin/app" }, |
| "facets": { |
| "fuchsia.test": { |
| "system-services": [ "fuchsia.logger.LogSink" ] |
| } |
| } |
| }), |
| result = Ok(()), |
| }, |
| test_cmx_block_system_data => { |
| input = json!({ |
| "program": { "binary": "bin/app" }, |
| "sandbox": { |
| "system": [ "data" ] |
| } |
| }), |
| result = Err(Error::validate_schema(CMX_SCHEMA, "Not condition is not met at /sandbox/system/0")), |
| }, |
| test_cmx_block_system_data_stem => { |
| input = json!({ |
| "program": { "binary": "bin/app" }, |
| "sandbox": { |
| "system": [ "data-should-pass" ] |
| } |
| }), |
| result = Ok(()), |
| }, |
| test_cmx_block_system_data_leading_slash => { |
| input = json!({ |
| "program": { "binary": "bin/app" }, |
| "sandbox": { |
| "system": [ "/data" ] |
| } |
| }), |
| result = Err(Error::validate_schema(CMX_SCHEMA, "Not condition is not met at /sandbox/system/0")), |
| }, |
| test_cmx_block_system_data_subdir => { |
| input = json!({ |
| "program": { "binary": "bin/app" }, |
| "sandbox": { |
| "system": [ "data/should-fail" ] |
| } |
| }), |
| result = Err(Error::validate_schema(CMX_SCHEMA, "Not condition is not met at /sandbox/system/0")), |
| }, |
| test_cmx_block_system_deprecated_data => { |
| input = json!({ |
| "program": { "binary": "bin/app" }, |
| "sandbox": { |
| "system": [ "deprecated-data" ] |
| } |
| }), |
| result = Err(Error::validate_schema(CMX_SCHEMA, "Not condition is not met at /sandbox/system/0")), |
| }, |
| test_cmx_block_system_deprecated_data_stem => { |
| input = json!({ |
| "program": { "binary": "bin/app" }, |
| "sandbox": { |
| "system": [ "deprecated-data-should-pass" ] |
| } |
| }), |
| result = Ok(()), |
| }, |
| test_cmx_block_system_deprecated_data_leading_slash => { |
| input = json!({ |
| "program": { "binary": "bin/app" }, |
| "sandbox": { |
| "system": [ "/deprecated-data" ] |
| } |
| }), |
| result = Err(Error::validate_schema(CMX_SCHEMA, "Not condition is not met at /sandbox/system/0")), |
| }, |
| test_cmx_block_system_deprecated_data_subdir => { |
| input = json!({ |
| "program": { "binary": "bin/app" }, |
| "sandbox": { |
| "system": [ "deprecated-data/should-fail" ] |
| } |
| }), |
| result = Err(Error::validate_schema(CMX_SCHEMA, "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 = $input:expr, |
| extra_schemas = $extra_schemas:expr, |
| result = $result:expr, |
| }, |
| )+ |
| ) => { |
| $( |
| #[test] |
| fn $test_name() -> Result<(), Error> { |
| validate_extra_schemas_test($input, $extra_schemas, $result) |
| } |
| )+ |
| } |
| } |
| |
| fn validate_extra_schemas_test( |
| input: serde_json::value::Value, |
| extra_schemas: &[(&JsonSchema<'_>, Option<String>)], |
| expected_result: Result<(), Error>, |
| ) -> Result<(), Error> { |
| let input_str = format!("{}", input); |
| let tmp_dir = TempDir::new()?; |
| let tmp_cmx_path = tmp_dir.path().join("test.cmx"); |
| File::create(&tmp_cmx_path)?.write_all(input_str.as_bytes())?; |
| |
| let extra_schema_paths = |
| extra_schemas.iter().map(|i| (Path::new(&*i.0.name), i.1.clone())).collect::<Vec<_>>(); |
| let result = validate(&[tmp_cmx_path.as_path()], &extra_schema_paths); |
| assert_eq!(format!("{:?}", result), format!("{:?}", expected_result)); |
| Ok(()) |
| } |
| |
| test_validate_extra_schemas! { |
| test_validate_extra_schemas_empty_json => { |
| input = json!({"program": {"binary": "a"}}), |
| extra_schemas = &[(&BLOCK_SHELL_FEATURE_SCHEMA, None)], |
| result = Ok(()), |
| }, |
| test_validate_extra_schemas_empty_features => { |
| input = json!({"sandbox": {"features": []}, "program": {"binary": "a"}}), |
| extra_schemas = &[(&BLOCK_SHELL_FEATURE_SCHEMA, None)], |
| result = Ok(()), |
| }, |
| test_validate_extra_schemas_feature_not_present => { |
| input = json!({"sandbox": {"features": ["isolated-persistent-storage"]}, "program": {"binary": "a"}}), |
| extra_schemas = &[(&BLOCK_SHELL_FEATURE_SCHEMA, None)], |
| result = Ok(()), |
| }, |
| test_validate_extra_schemas_feature_present => { |
| input = json!({"sandbox": {"features" : ["deprecated-shell"]}, "program": {"binary": "a"}}), |
| extra_schemas = &[(&BLOCK_SHELL_FEATURE_SCHEMA, None)], |
| result = Err(Error::validate_schema(&BLOCK_SHELL_FEATURE_SCHEMA, "Not condition is not met at /sandbox/features/0")), |
| }, |
| test_validate_extra_schemas_block_dev => { |
| input = json!({"dev": ["misc"], "program": {"binary": "a"}}), |
| extra_schemas = &[(&BLOCK_DEV_SCHEMA, None)], |
| result = Err(Error::validate_schema(&BLOCK_DEV_SCHEMA, "Not condition is not met at /dev")), |
| }, |
| test_validate_multiple_extra_schemas_valid => { |
| input = json!({"sandbox": {"features": ["isolated-persistent-storage"]}, "program": {"binary": "a"}}), |
| extra_schemas = &[(&BLOCK_SHELL_FEATURE_SCHEMA, None), (&BLOCK_DEV_SCHEMA, None)], |
| result = Ok(()), |
| }, |
| test_validate_multiple_extra_schemas_invalid => { |
| input = json!({"dev": ["misc"], "sandbox": {"features": ["isolated-persistent-storage"]}, "program": {"binary": "a"}}), |
| extra_schemas = &[(&BLOCK_SHELL_FEATURE_SCHEMA, None), (&BLOCK_DEV_SCHEMA, None)], |
| result = Err(Error::validate_schema(&BLOCK_DEV_SCHEMA, "Not condition is not met at /dev")), |
| }, |
| } |
| |
| #[test] |
| fn test_validate_extra_error() -> Result<(), Error> { |
| validate_extra_schemas_test( |
| json!({"dev": ["misc"], "program": {"binary": "a"}}), |
| &[(&BLOCK_DEV_SCHEMA, Some("Extra error".to_string()))], |
| Err(Error::validate_schema( |
| &BLOCK_DEV_SCHEMA, |
| "Not condition is not met at /dev\nExtra error", |
| )), |
| ) |
| } |
| |
| fn empty_offer() -> cml::Offer { |
| cml::Offer { |
| service: None, |
| protocol: None, |
| directory: None, |
| storage: None, |
| runner: None, |
| from: cml::Ref::Self_, |
| to: vec![], |
| r#as: None, |
| rights: None, |
| } |
| } |
| |
| #[test] |
| fn test_capability_id() -> Result<(), Error> { |
| // Simple tests. |
| assert_eq!( |
| CapabilityId::from_clause(&cml::Offer { |
| service: Some("/a".to_string()), |
| ..empty_offer() |
| })?, |
| vec![CapabilityId::Path("/a")] |
| ); |
| assert_eq!( |
| CapabilityId::from_clause(&cml::Offer { |
| protocol: Some(OneOrMany::One("/a".to_string())), |
| ..empty_offer() |
| })?, |
| vec![CapabilityId::Path("/a")] |
| ); |
| assert_eq!( |
| CapabilityId::from_clause(&cml::Offer { |
| protocol: Some(OneOrMany::Many(vec!["/a".to_string(), "/b".to_string()])), |
| ..empty_offer() |
| })?, |
| vec![CapabilityId::Path("/a"), CapabilityId::Path("/b")] |
| ); |
| assert_eq!( |
| CapabilityId::from_clause(&cml::Offer { |
| directory: Some("/a".to_string()), |
| ..empty_offer() |
| })?, |
| vec![CapabilityId::Path("/a")] |
| ); |
| assert_eq!( |
| CapabilityId::from_clause(&cml::Offer { |
| storage: Some("a".to_string()), |
| ..empty_offer() |
| })?, |
| vec![CapabilityId::StorageType("a")] |
| ); |
| |
| // "as" aliasing. |
| assert_eq!( |
| CapabilityId::from_clause(&cml::Offer { |
| service: Some("/a".to_string()), |
| r#as: Some("/b".to_string()), |
| ..empty_offer() |
| })?, |
| vec![CapabilityId::Path("/b")] |
| ); |
| |
| // Error case. |
| assert_matches!(CapabilityId::from_clause(&empty_offer()), Err(_)); |
| |
| Ok(()) |
| } |
| } |