blob: bb0913c3cfd073c67b8aa8ce551c0d7f998ea32d [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::cml;
use cm_json::{self, Error, CML_SCHEMA, CMX_SCHEMA, CM_SCHEMA};
use serde_json::Value;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::io::Read;
use std::path::PathBuf;
/// Read in and parse a list of files, and return an Error if any of the given files are not valid
/// cmx. One of the JSON schemas located at ../*_schema.json, selected based on the file extension,
/// is used to determine the validity of each input file.
pub fn validate(files: Vec<PathBuf>) -> Result<(), Error> {
const BAD_EXTENSION: &str = "Input file does not have a component manifest extension \
(.cm, .cml, or .cmx)";
if files.is_empty() {
return Err(Error::invalid_args("No files provided"));
}
for filename in files {
let mut buffer = String::new();
fs::File::open(&filename)?.read_to_string(&mut buffer)?;
match filename.extension().and_then(|e| e.to_str()) {
Some("cm") => {
let v: Value = serde_json::from_str(&buffer)
.map_err(|e| Error::parse(format!("Couldn't read input as JSON: {}", e)))?;
cm_json::validate_json(&v, CM_SCHEMA)
}
Some("cml") => validate_cml(&buffer).map(|_d| ()),
Some("cmx") => {
let v: Value = serde_json::from_str(&buffer)
.map_err(|e| Error::parse(format!("Couldn't read input as JSON: {}", e)))?;
cm_json::validate_json(&v, CMX_SCHEMA)
}
_ => Err(Error::invalid_args(BAD_EXTENSION)),
}?;
}
Ok(())
}
/// Validates CML JSON document according to the schema and returns it as a cml::Document.
pub fn validate_cml(buffer: &str) -> Result<cml::Document, Error> {
let json: Value = cm_json::from_json5_str(&buffer)?;
cm_json::validate_json(&json, CML_SCHEMA)?;
let document: cml::Document = serde_json::from_value(json)
.map_err(|e| Error::parse(format!("Couldn't read input as struct: {}", e)))?;
let mut ctx = ValidationContext {
document: &document,
all_children: HashSet::new(),
};
ctx.validate()?;
Ok(document)
}
struct ValidationContext<'a> {
document: &'a cml::Document,
all_children: HashSet<&'a str>,
}
type PathMap<'a> = HashMap<String, HashSet<&'a str>>;
impl<'a> ValidationContext<'a> {
fn validate(&mut self) -> Result<(), Error> {
// Get the set of all children.
if let Some(children) = self.document.children.as_ref() {
for child in children.iter() {
if !self.all_children.insert(&child.name) {
return Err(Error::parse(format!(
"Duplicate child name: \"{}\"",
&child.name
)));
}
}
}
// Validate "expose".
if let Some(exposes) = self.document.expose.as_ref() {
let mut target_paths = HashMap::new();
for expose in exposes.iter() {
self.validate_expose(&expose, &mut target_paths)?;
}
}
// Validate "offer".
if let Some(offers) = self.document.offer.as_ref() {
let mut target_paths = HashMap::new();
for offer in offers.iter() {
self.validate_offer(&offer, &mut target_paths)?;
}
}
Ok(())
}
fn validate_expose(
&self, expose: &'a cml::Expose, prev_target_paths: &mut PathMap<'a>,
) -> Result<(), Error> {
self.validate_source("expose", expose)?;
self.validate_target(
"expose",
expose,
expose,
&mut HashSet::new(),
prev_target_paths,
)
}
fn validate_offer(
&self, offer: &'a cml::Offer, prev_target_paths: &mut PathMap<'a>,
) -> Result<(), Error> {
self.validate_source("offer", offer)?;
let mut prev_targets = HashSet::new();
for target in offer.targets.iter() {
// Check that any referenced child in the target name is valid.
if let Some(caps) = cml::CHILD_RE.captures(&target.to) {
if !self.all_children.contains(&caps[1]) {
return Err(Error::parse(format!(
"\"{}\" is an \"offer\" target but it does not appear in \"children\"",
&target.to,
)));
}
}
self.validate_target("offer", offer, target, &mut prev_targets, prev_target_paths)?;
}
Ok(())
}
/// Validates that a source capability is valid, i.e. that any referenced child is valid.
/// - |keyword| is the keyword for the clause ("offer" or "expose").
fn validate_source<T>(&self, keyword: &str, source_obj: &'a T) -> Result<(), Error>
where
T: cml::FromClause + cml::CapabilityClause,
{
if let Some(caps) = cml::CHILD_RE.captures(source_obj.from()) {
if !self.all_children.contains(&caps[1]) {
return Err(Error::parse(format!(
"\"{}\" is an \"{}\" source but it does not appear in \"children\"",
source_obj.from(),
keyword,
)));
}
}
Ok(())
}
/// Validates that a target is valid, i.e. that it does not duplicate the path of any capability
/// and any referenced child is valid.
/// - |keyword| is the keyword for the clause ("offer" or "expose").
/// - |source_obj| is the object containing the source capability info. This is needed for the
/// default path.
/// - |target_obj| is the object containing the target capability info.
/// - |prev_target| holds target names collected for this source capability so far.
/// - |prev_target_paths| holds target paths collected so far.
fn validate_target<T, U>(
&self, keyword: &str, source_obj: &'a T, target_obj: &'a U,
prev_targets: &mut HashSet<&'a str>, prev_target_paths: &mut PathMap<'a>,
) -> Result<(), Error>
where
T: cml::CapabilityClause,
U: cml::ToClause + cml::AsClause,
{
// Get the source capability's path.
let source_path = if let Some(p) = source_obj.service().as_ref() {
p
} else if let Some(p) = source_obj.directory().as_ref() {
p
} else {
return Err(Error::internal(format!("no capability path")));
};
// Get the target capability's path (defaults to the source path).
let ref target_path = match &target_obj.r#as() {
Some(a) => a,
None => source_path,
};
// Check that target path is not a duplicate of another capability.
let target_name = target_obj.to().unwrap_or("");
let paths_for_target = prev_target_paths
.entry(target_name.to_string())
.or_insert(HashSet::new());
if !paths_for_target.insert(target_path) {
return match target_name {
"" => Err(Error::parse(format!(
"\"{}\" is a duplicate \"{}\" target path",
target_path, keyword
))),
_ => Err(Error::parse(format!(
"\"{}\" is a duplicate \"{}\" target path for \"{}\"",
target_path, keyword, target_name
))),
};
}
// Check that the target is not a duplicate of a previous target (for this source).
if let Some(target_name) = target_obj.to() {
if !prev_targets.insert(target_name) {
return Err(Error::parse(format!(
"\"{}\" is a duplicate \"{}\" target for \"{}\"",
target_name, keyword, source_path
)));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::fs::File;
use std::io::Write;
use tempfile::TempDir;
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));
}
// TODO(CF-167): fix JSON5 int->float parse bug
#[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 {
// This should be assert!(n.is_i64()), but the json5 parser has a bug that parses all
// numbers as floats.
assert!(!n.is_i64() && !n.is_u64() && n.is_f64());
} else {
panic!("not a number");
}
}
// 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": [
{
"type": "service",
"source_path": "/svc/fuchsia.boot.Log",
"target_path": "/svc/fuchsia.logger.Log",
},
{
"type": "directory",
"source_path": "/data/assets",
"target_path": "/data/kitten_assets",
}
]
}),
result = Ok(()),
},
test_cm_uses_missing_props => {
input = json!({
"uses": [ {} ]
}),
result = Err(Error::parse("This property is required at /uses/0/source_path, This property is required at /uses/0/target_path, This property is required at /uses/0/type")),
},
test_cm_uses_bad_type => {
input = json!({
"uses": [
{
"type": "bad",
"source_path": "/svc/fuchsia.logger.Log",
"target_path": "/svc/fuchsia.logger.Log",
}
]
}),
result = Err(Error::parse("Pattern condition is not met at /uses/0/type")),
},
// exposes
test_cm_exposes => {
input = json!({
"exposes": [
{
"type": "service",
"source_path": "/svc/fuchsia.ui.Scenic",
"source": {
"relation": "self"
},
"target_path": "/svc/fuchsia.ui.Scenic"
},
{
"type": "directory",
"source_path": "/data/assets",
"source": {
"relation": "child",
"child_name": "cat_viewer"
},
"target_path": "/data/kitten_assets"
}
]
}),
result = Ok(()),
},
test_cm_exposes_missing_props => {
input = json!({
"exposes": [ {} ]
}),
result = Err(Error::parse("This property is required at /exposes/0/source, This property is required at /exposes/0/source_path, This property is required at /exposes/0/target_path, This property is required at /exposes/0/type")),
},
test_cm_exposes_bad_type => {
input = json!({
"exposes": [
{
"type": "bad",
"source_path": "/svc/fuchsia.ui.Scenic",
"source": {
"relation": "self"
},
"target_path": "/svc/fuchsia.ui.Scenic"
}
]
}),
result = Err(Error::parse("Pattern condition is not met at /exposes/0/type")),
},
test_cm_exposes_source_missing_props => {
input = json!({
"exposes": [
{
"type": "service",
"source_path": "/svc/fuchsia.ui.Scenic",
"source": {},
"target_path": "/svc/fuchsia.ui.Scenic"
}
]
}),
result = Err(Error::parse("This property is required at /exposes/0/source/relation")),
},
test_cm_exposes_source_extraneous_child => {
input = json!({
"exposes": [
{
"type": "service",
"source_path": "/svc/fuchsia.ui.Scenic",
"source": { "relation": "self", "child_name": "scenic" },
"target_path": "/svc/fuchsia.ui.Scenic"
}
]
}),
result = Err(Error::parse("OneOf conditions are not met at /exposes/0/source")),
},
test_cm_exposes_source_missing_child => {
input = json!({
"exposes": [
{
"type": "service",
"source_path": "/svc/fuchsia.ui.Scenic",
"source": { "relation": "child" },
"target_path": "/svc/fuchsia.ui.Scenic"
}
]
}),
result = Err(Error::parse("OneOf conditions are not met at /exposes/0/source")),
},
test_cm_exposes_source_bad_relation => {
input = json!({
"exposes": [
{
"type": "service",
"source_path": "/svc/fuchsia.ui.Scenic",
"source": {
"relation": "realm"
},
"target_path": "/svc/fuchsia.ui.Scenic"
}
]
}),
result = Err(Error::parse("Pattern condition is not met at /exposes/0/source/relation")),
},
test_cm_exposes_source_bad_child_name => {
input = json!({
"exposes": [
{
"type": "service",
"source_path": "/svc/fuchsia.ui.Scenic",
"source": {
"relation": "child",
"child_name": "bad^"
},
"target_path": "/svc/fuchsia.ui.Scenic"
}
]
}),
result = Err(Error::parse("Pattern condition is not met at /exposes/0/source/child_name")),
},
// offers
test_cm_offers => {
input = json!({
"offers": [
{
"type": "service",
"source_path": "/svc/fuchsia.logger.LogSink",
"source": {
"relation": "realm"
},
"targets": [
{
"target_path": "/svc/fuchsia.logger.SysLog",
"child_name": "viewer"
}
]
},
{
"type": "service",
"source_path": "/svc/fuchsia.ui.Scenic",
"source": {
"relation": "self"
},
"targets": [
{
"target_path": "/svc/fuchsia.ui.Scenic",
"child_name": "user_shell"
},
{
"target_path": "/services/fuchsia.ui.Scenic",
"child_name": "viewer"
}
]
},
{
"type": "directory",
"source_path": "/data/assets",
"source": {
"relation": "child",
"child_name": "cat_provider"
},
"targets": [
{
"target_path": "/data/kitten_assets",
"child_name": "cat_viewer"
}
]
}
]
}),
result = Ok(()),
},
test_cm_offers_all_valid_chars => {
input = json!({
"offers": [
{
"type": "service",
"source_path": "/svc/fuchsia.logger.LogSink",
"source": {
"relation": "child",
"child_name": "abcdefghijklmnopqrstuvwxyz0123456789_-."
},
"targets": [
{
"target_path": "/svc/fuchsia.logger.SysLog",
"child_name": "abcdefghijklmnopqrstuvwxyz0123456789_-."
}
]
}
]
}),
result = Ok(()),
},
test_cm_offers_missing_props => {
input = json!({
"offers": [ {} ]
}),
result = Err(Error::parse("This property is required at /offers/0/source, This property is required at /offers/0/source_path, This property is required at /offers/0/targets, This property is required at /offers/0/type")),
},
test_cm_offers_bad_type => {
input = json!({
"offers": [
{
"type": "bad",
"source_path": "/svc/fuchsia.ui.Scenic",
"source": {
"relation": "self"
},
"targets": [
{
"target_path": "/svc/fuchsia.ui.Scenic",
"child_name": "user_shell"
}
]
}
]
}),
result = Err(Error::parse("Pattern condition is not met at /offers/0/type")),
},
test_cm_offers_source_missing_props => {
input = json!({
"offers": [
{
"type": "service",
"source_path": "/svc/fuchsia.ui.Scenic",
"source": {},
"targets": [
{
"target_path": "/svc/fuchsia.ui.Scenic",
"child_name": "user_shell"
}
]
}
]
}),
result = Err(Error::parse("This property is required at /offers/0/source/relation")),
},
test_cm_offers_source_extraneous_child => {
input = json!({
"offers": [
{
"type": "service",
"source_path": "/svc/fuchsia.ui.Scenic",
"source": { "relation": "self", "child_name": "scenic" },
"targets": [
{
"target_path": "/svc/fuchsia.ui.Scenic",
"child_name": "user_shell"
}
]
}
]
}),
result = Err(Error::parse("OneOf conditions are not met at /offers/0/source")),
},
test_cm_offers_source_missing_child => {
input = json!({
"offers": [
{
"type": "service",
"source_path": "/svc/fuchsia.ui.Scenic",
"source": { "relation": "child" },
"targets": [
{
"target_path": "/svc/fuchsia.ui.Scenic",
"child_name": "user_shell"
}
]
}
]
}),
result = Err(Error::parse("OneOf conditions are not met at /offers/0/source")),
},
test_cm_offers_source_bad_relation => {
input = json!({
"offers": [
{
"type": "service",
"source_path": "/svc/fuchsia.ui.Scenic",
"source": {
"relation": "bad"
},
"targets": [
{
"target_path": "/svc/fuchsia.ui.Scenic",
"child_name": "user_shell"
}
]
}
]
}),
result = Err(Error::parse("Pattern condition is not met at /offers/0/source/relation")),
},
test_cm_offers_source_bad_child_name => {
input = json!({
"offers": [
{
"type": "service",
"source_path": "/svc/fuchsia.ui.Scenic",
"source": {
"relation": "child",
"child_name": "bad^"
},
"targets": [
{
"target_path": "/svc/fuchsia.ui.Scenic",
"child_name": "user_shell"
}
]
}
]
}),
result = Err(Error::parse("Pattern condition is not met at /offers/0/source/child_name")),
},
test_cm_offers_target_missing_props => {
input = json!({
"offers": [
{
"type": "service",
"source_path": "/svc/fuchsia.ui.Scenic",
"source": {
"relation": "child",
"child_name": "cat_viewer"
},
"targets": [ {} ]
}
]
}),
result = Err(Error::parse("This property is required at /offers/0/targets/0/child_name, This property is required at /offers/0/targets/0/target_path")),
},
test_cm_offers_target_bad_child_name => {
input = json!({
"offers": [
{
"type": "service",
"source_path": "/svc/fuchsia.ui.Scenic",
"source": {
"relation": "self"
},
"targets": [
{
"target_path": "/svc/fuchsia.ui.Scenic",
"child_name": "bad^"
}
]
}
]
}),
result = Err(Error::parse("Pattern condition is not met at /offers/0/targets/0/child_name")),
},
// children
test_cm_children => {
input = json!({
"children": [
{
"name": "system-logger2",
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
},
{
"name": "abc123_-",
"uri": "https://www.google.com/gmail"
}
]
}),
result = Ok(()),
},
test_cm_children_missing_props => {
input = json!({
"children": [ {} ]
}),
result = Err(Error::parse("This property is required at /children/0/name, This property is required at /children/0/uri")),
},
test_cm_children_bad_name => {
input = json!({
"children": [
{
"name": "bad^",
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
}
]
}),
result = Err(Error::parse("Pattern condition is not met at /children/0/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::parse("Type of the value is wrong at /facets")),
},
// constraints
test_cm_path => {
input = json!({
"uses": [
{
"type": "directory",
"source_path": "/foo/?!@#$%/Bar",
"target_path": "/bar/&*()/Baz"
}
]
}),
result = Ok(()),
},
test_cm_path_invalid => {
input = json!({
"uses": [
{
"type": "directory",
"source_path": "foo/",
"target_path": "/bar"
}
]
}),
result = Err(Error::parse("Pattern condition is not met at /uses/0/source_path")),
},
test_cm_path_too_long => {
input = json!({
"uses": [
{
"type": "directory",
"source_path": "/".repeat(1025),
"target_path": "/bar"
}
]
}),
result = Err(Error::parse("MaxLength condition is not met at /uses/0/source_path")),
},
test_cm_name => {
input = json!({
"children": [
{
"name": "abcdefghijklmnopqrstuvwxyz0123456789_-.",
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
}
]
}),
result = Ok(()),
},
test_cm_name_invalid => {
input = json!({
"children": [
{
"name": "#bad",
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
}
]
}),
result = Err(Error::parse("Pattern condition is not met at /children/0/name")),
},
test_cm_name_too_long => {
input = json!({
"children": [
{
"name": "a".repeat(101),
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
}
]
}),
result = Err(Error::parse("MaxLength condition is not met at /children/0/name")),
},
test_cm_uri => {
input = json!({
"children": [
{
"name": "logger",
"uri": "my+awesome-scheme.2://abc123!@#$%.com"
}
]
}),
result = Ok(()),
},
test_cm_uri_invalid => {
input = json!({
"children": [
{
"name": "logger",
"uri": "fuchsia-pkg://"
}
]
}),
result = Err(Error::parse("Pattern condition is not met at /children/0/uri")),
},
test_cm_uri_too_long => {
input = json!({
"children": [
{
"name": "logger",
"uri": &format!("fuchsia-pkg://{}", "a".repeat(4083))
}
]
}),
result = Err(Error::parse("MaxLength condition is not met at /children/0/uri")),
},
}
#[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", },
],
"children": [
{
'name': 'logger',
'uri': '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::parse("This property is required at /program/binary")),
},
// use
test_cml_use => {
input = json!({
"use": [
{ "service": "/fonts/CoolFonts", "as": "/svc/fuchsia.fonts.Provider" },
{ "directory": "/data/assets" }
]
}),
result = Ok(()),
},
test_cml_use_missing_props => {
input = json!({
"use": [ { "as": "/svc/fuchsia.logger.Log" } ]
}),
result = Err(Error::parse("OneOf conditions are not met at /use/0")),
},
// expose
test_cml_expose => {
input = json!({
"expose": [
{
"service": "/loggers/fuchsia.logger.Log",
"from": "#logger",
"as": "/svc/logger"
},
{ "directory": "/volumes/blobfs", "from": "self" }
],
"children": [
{
"name": "logger",
"uri": "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_-.",
"uri": "https://www.google.com/gmail"
}
]
}),
result = Ok(()),
},
test_cml_expose_missing_props => {
input = json!({
"expose": [ {} ]
}),
result = Err(Error::parse("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::parse("\"#missing\" is an \"expose\" source but it 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" }
],
"children": [
{
"name": "logger",
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
}
]
}),
result = Err(Error::parse("\"/thing\" is a duplicate \"expose\" target path")),
},
test_cml_expose_bad_from => {
input = json!({
"expose": [ {
"service": "/loggers/fuchsia.logger.Log", "from": "realm"
} ]
}),
result = Err(Error::parse("Pattern condition is not met at /expose/0/from")),
},
// offer
test_cml_offer => {
input = json!({
"offer": [
{
"service": "/svc/fuchsia.logger.Log",
"from": "#logger",
"targets": [
{ "to": "#echo2_server" },
{ "to": "#scenic", "as": "/svc/fuchsia.logger.SysLog" }
]
},
{
"service": "/svc/fuchsia.fonts.Provider",
"from": "realm",
"targets": [
{ "to": "#echo2_server" },
]
},
{
"directory": "/data/assets",
"from": "self",
"targets": [
{ "to": "#echo2_server" },
]
}
],
"children": [
{
"name": "logger",
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
},
{
"name": "scenic",
"uri": "fuchsia-pkg://fuchsia.com/scenic/stable#meta/scenic.cm"
},
{
"name": "echo2_server",
"uri": "fuchsia-pkg://fuchsia.com/echo2/stable#meta/echo2_server.cm"
}
]
}),
result = Ok(()),
},
test_cml_offer_all_valid_chars => {
input = json!({
"offer": [
{
"service": "/svc/fuchsia.logger.Log",
"from": "#abcdefghijklmnopqrstuvwxyz0123456789_-",
"targets": [
{
"to": "#abcdefghijklmnopqrstuvwxyz0123456789_-"
}
]
}
],
"children": [
{
"name": "abcdefghijklmnopqrstuvwxyz0123456789_-",
"uri": "https://www.google.com/gmail"
}
]
}),
result = Ok(()),
},
test_cml_offer_missing_props => {
input = json!({
"offer": [ {} ]
}),
result = Err(Error::parse("OneOf conditions are not met at /offer/0, This property is required at /offer/0/from, This property is required at /offer/0/targets")),
},
test_cml_offer_missing_from => {
input = json!({
"offer": [ {
"service": "/svc/fuchsia.logger.Log",
"from": "#missing",
"targets": [
{ "to": "#echo2_server" },
]
} ]
}),
result = Err(Error::parse("\"#missing\" is an \"offer\" source but it does not appear in \"children\"")),
},
test_cml_offer_bad_from => {
input = json!({
"offer": [ {
"service": "/svc/fuchsia.logger.Log",
"from": "#invalid@",
"targets": [
{ "to": "#echo2_server" },
]
} ]
}),
result = Err(Error::parse("Pattern condition is not met at /offer/0/from")),
},
test_cml_offer_empty_targets => {
input = json!({
"offer": [ {
"service": "/svc/fuchsia.logger.Log",
"from": "#logger",
"targets": []
} ]
}),
result = Err(Error::parse("MinItems condition is not met at /offer/0/targets")),
},
test_cml_offer_target_missing_props => {
input = json!({
"offer": [ {
"service": "/svc/fuchsia.logger.Log",
"from": "#logger",
"targets": [
{ "as": "/svc/fuchsia.logger.SysLog" }
]
} ]
}),
result = Err(Error::parse("This property is required at /offer/0/targets/0/to")),
},
test_cml_offer_target_missing_to => {
input = json!({
"offer": [ {
"service": "/snvc/fuchsia.logger.Log",
"from": "#logger",
"targets": [
{ "to": "#missing" }
]
} ],
"children": [ {
"name": "logger",
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
} ]
}),
result = Err(Error::parse("\"#missing\" is an \"offer\" target but it does not appear in \"children\"")),
},
test_cml_offer_target_bad_to => {
input = json!({
"offer": [ {
"service": "/svc/fuchsia.logger.Log",
"from": "#logger",
"targets": [
{ "to": "self", "as": "/svc/fuchsia.logger.SysLog" }
]
} ]
}),
result = Err(Error::parse("Pattern condition is not met at /offer/0/targets/0/to")),
},
test_cml_offer_duplicate_target_paths => {
input = json!({
"offer": [
{
"service": "/svc/logger",
"from": "self",
"targets": [
{ "to": "#echo2_server", "as": "/thing" },
{ "to": "#scenic" }
]
},
{
"directory": "/thing",
"from": "realm",
"targets": [
{ "to": "#echo2_server" }
]
}
],
"children": [
{
"name": "scenic",
"uri": "fuchsia-pkg://fuchsia.com/scenic/stable#meta/scenic.cm"
},
{
"name": "echo2_server",
"uri": "fuchsia-pkg://fuchsia.com/echo2/stable#meta/echo2_server.cm"
}
]
}),
result = Err(Error::parse("\"/thing\" is a duplicate \"offer\" target path for \"#echo2_server\"")),
},
// children
test_cml_children => {
input = json!({
"children": [
{
"name": "logger",
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
},
{
"name": "gmail",
"uri": "https://www.google.com/gmail"
}
]
}),
result = Ok(()),
},
test_cml_children_missing_props => {
input = json!({
"children": [ {} ]
}),
result = Err(Error::parse("This property is required at /children/0/name, This property is required at /children/0/uri")),
},
test_cml_children_duplicate_names => {
input = json!({
"children": [
{
"name": "logger",
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
},
{
"name": "logger",
"uri": "fuchsia-pkg://fuchsia.com/logger/beta#meta/logger.cm"
}
]
}),
result = Err(Error::parse("Duplicate child name: \"logger\"")),
},
// 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::parse("Type of the value is wrong at /facets")),
},
// constraints
test_cml_path => {
input = json!({
"use": [
{ "directory": "/foo/?!@#$%/Bar" },
]
}),
result = Ok(()),
},
test_cml_path_invalid => {
input = json!({
"use": [
{ "service": "foo/" },
]
}),
result = Err(Error::parse("Pattern condition is not met at /use/0/service")),
},
test_cml_path_too_long => {
input = json!({
"use": [
{ "service": "/".repeat(1025) },
]
}),
result = Err(Error::parse("MaxLength condition is not met at /use/0/service")),
},
test_cml_relative_id_too_long => {
input = json!({
"expose": [
{
"service": "/loggers/fuchsia.logger.Log",
"from": &format!("#{}", "a".repeat(101)),
},
],
"children": [
{
"name": "logger",
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
},
]
}),
result = Err(Error::parse("MaxLength condition is not met at /expose/0/from")),
},
test_cml_child_name => {
input = json!({
"children": [
{
"name": "abcdefghijklmnopqrstuvwxyz0123456789_-.",
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
},
]
}),
result = Ok(()),
},
test_cml_child_name_invalid => {
input = json!({
"children": [
{
"name": "#bad",
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
},
]
}),
result = Err(Error::parse("Pattern condition is not met at /children/0/name")),
},
test_cml_child_name_too_long => {
input = json!({
"children": [
{
"name": "a".repeat(101),
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm",
}
]
}),
result = Err(Error::parse("MaxLength condition is not met at /children/0/name")),
},
test_cml_uri => {
input = json!({
"children": [
{
"name": "logger",
"uri": "my+awesome-scheme.2://abc123!@#$%.com",
},
]
}),
result = Ok(()),
},
test_cml_uri_invalid => {
input = json!({
"children": [
{
"name": "logger",
"uri": "fuchsia-pkg://",
},
]
}),
result = Err(Error::parse("Pattern condition is not met at /children/0/uri")),
},
test_cml_uri_too_long => {
input = json!({
"children": [
{
"name": "logger",
"uri": &format!("fuchsia-pkg://{}", "a".repeat(4083)),
},
]
}),
result = Err(Error::parse("MaxLength condition is not met at /children/0/uri")),
},
}
test_validate_cmx! {
test_cmx_err_empty_json => {
input = json!({}),
result = Err(Error::parse("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::parse("OneOf conditions are not met at /program")),
},
test_cmx_bad_program => {
input = json!({"prigram": { "binary": "bin/app" }}),
result = Err(Error::parse("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(()),
},
}
}