blob: fff8848391aee27ef89c38740406a303998755a3 [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, 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::path::Path;
/// 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> {
cm_json::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(),
};
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)?;
cm_json::validate_json(&v, CMX_SCHEMA)?;
v
}
Some("cm") => {
let v = cm_json::from_json_str(&buffer)?;
cm_json::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())?;
cm_json::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(())
}
struct ValidationContext<'a> {
document: &'a cml::Document,
all_children: HashSet<&'a str>,
all_collections: HashSet<&'a str>,
}
type PathMap<'a> = HashMap<String, HashSet<&'a str>>;
impl<'a> ValidationContext<'a> {
fn validate(&mut self) -> Result<(), Error> {
// Populate the sets of children and collections.
self.all_children = self.document.all_children()?;
self.all_collections = self.document.all_collections()?;
// 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 from_caps = cml::REFERENCE_RE.captures(&offer.from);
let from_child = match &from_caps {
Some(caps) => Some(&caps[0]),
None => None,
};
let mut prev_targets = HashSet::new();
for to in offer.to.iter() {
// Check that any referenced child in the target name is valid.
if let Some(caps) = cml::REFERENCE_RE.captures(&to.dest) {
if !self.all_children.contains(&caps[1]) && !self.all_collections.contains(&caps[1])
{
return Err(Error::validate(format!(
"\"{}\" is an \"offer\" target but it does not appear in \"children\" \
or \"collections\"",
&to.dest,
)));
}
}
// Check that the capability is not being re-offered to a target that exposed it.
if let Some(from_child) = &from_child {
if from_child == &to.dest {
return Err(Error::validate(format!(
"Offer target \"{}\" is same as source",
&to.dest,
)));
}
}
// Perform common target validation.
self.validate_target("offer", offer, to, &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::REFERENCE_RE.captures(source_obj.from()) {
if !self.all_children.contains(&caps[1]) {
return Err(Error::validate(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::DestClause + 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.dest().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::validate(format!(
"\"{}\" is a duplicate \"{}\" target path",
target_path, keyword
))),
_ => Err(Error::validate(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.dest() {
if !prev_targets.insert(target_name) {
return Err(Error::validate(format!(
"\"{}\" is a duplicate \"{}\" target for \"{}\"",
target_name, keyword, source_path
)));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use lazy_static::lazy_static;
use serde_json::json;
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));
}
#[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_path": "/svc/fuchsia.boot.Log",
"target_path": "/svc/fuchsia.logger.Log"
}
},
{
"directory": {
"source_path": "/data/assets",
"target_path": "/data/kitten_assets"
}
}
]
}),
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": {
"myself": {}
},
"target_path": "/svc/fuchsia.ui.Scenic"
}
},
{
"directory": {
"source_path": "/data/assets",
"source": {
"child": {
"name": "cat_viewer"
}
},
"target_path": "/data/kitten_assets"
}
}
]
}),
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"
}
}
]
}),
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": {
"myself": {},
"child": {
"name": "foo"
}
},
"target_path": "/svc/fuchsia.ui.Scenic"
}
}
]
}),
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"
}
}
]
}),
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_path": "/svc/fuchsia.logger.LogSink",
"source": {
"realm": {}
},
"targets": [
{
"target_path": "/svc/fuchsia.logger.SysLog",
"dest": {
"child": {
"name": "viewer"
}
}
}
]
}
},
{
"service": {
"source_path": "/svc/fuchsia.ui.Scenic",
"source": {
"myself": {}
},
"targets": [
{
"target_path": "/svc/fuchsia.ui.Scenic",
"dest": {
"child": {
"name": "user_shell"
}
}
},
{
"target_path": "/services/fuchsia.ui.Scenic",
"dest": {
"collection": {
"name": "modular"
}
}
}
]
}
},
{
"directory": {
"source_path": "/data/assets",
"source": {
"child": {
"name": "cat_provider"
}
},
"targets": [
{
"target_path": "/data/kitten_assets",
"dest": {
"child": {
"name": "cat_viewer"
}
}
},
{
"target_path": "/data/artifacts",
"dest": {
"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_-."
}
},
"targets": [
{
"target_path": "/svc/fuchsia.logger.SysLog",
"dest": {
"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": {},
"targets": [
{
"target_path": "/svc/fuchsia.ui.Scenic",
"dest": {
"child": {
"name": "user_shell"
}
}
}
]
}
}
]
}),
result = Err(Error::validate_schema(CM_SCHEMA, "OneOf conditions are not met at /offers/0/service/source")),
},
test_cm_offers_source_multiple_variants => {
input = json!({
"offers": [
{
"service": {
"source_path": "/svc/fuchsia.ui.Scenic",
"source": {
"myself": {},
"child": {
"name": "foo"
}
},
"targets": [
{
"target_path": "/svc/fuchsia.ui.Scenic",
"dest": {
"child": {
"name": "user_shell"
}
}
}
]
}
}
]
}),
result = Err(Error::validate_schema(CM_SCHEMA, "OneOf conditions are not met at /offers/0/service/source")),
},
test_cm_offers_source_bad_child_name => {
input = json!({
"offers": [
{
"service": {
"source_path": "/svc/fuchsia.ui.Scenic",
"source": {
"child": {
"name": "bad^"
}
},
"targets": [
{
"target_path": "/svc/fuchsia.ui.Scenic",
"dest": {
"child": {
"name": "user_shell"
}
}
}
]
}
}
]
}),
result = Err(Error::validate_schema(CM_SCHEMA, "Pattern condition is not met at /offers/0/service/source/child/name")),
},
test_cm_offers_target_missing_props => {
input = json!({
"offers": [
{
"service": {
"source_path": "/svc/fuchsia.ui.Scenic",
"source": {
"child": {
"name": "cat_viewer"
}
},
"targets": [ {} ]
}
}
]
}),
result = Err(Error::validate_schema(CM_SCHEMA, "This property is required at /offers/0/service/targets/0/dest, This property is required at /offers/0/service/targets/0/target_path")),
},
test_cm_offers_target_bad_child_name => {
input = json!({
"offers": [
{
"service": {
"source_path": "/svc/fuchsia.ui.Scenic",
"source": {
"myself": {}
},
"targets": [
{
"target_path": "/svc/fuchsia.ui.Scenic",
"dest": {
"child": {
"name": "bad^"
}
}
}
]
}
}
]
}),
result = Err(Error::validate_schema(CM_SCHEMA, "Pattern condition is not met at /offers/0/service/targets/0/dest/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")),
},
// 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_path": "/foo/?!@#$%/Bar",
"target_path": "/bar/&*()/Baz"
}
}
]
}),
result = Ok(()),
},
test_cm_path_invalid_empty => {
input = json!({
"uses": [
{
"directory": {
"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_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_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_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_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", },
],
"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" },
{ "directory": "/data/assets" }
]
}),
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")),
},
// 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",
"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("\"#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",
"url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
}
]
}),
result = Err(Error::validate("\"/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::validate_schema(CML_SCHEMA, "Pattern condition is not met at /expose/0/from")),
},
// offer
test_cml_offer => {
input = json!({
"offer": [
{
"service": "/svc/fuchsia.logger.Log",
"from": "#logger",
"to": [
{ "dest": "#echo_server" },
{ "dest": "#modular", "as": "/svc/fuchsia.logger.SysLog" }
]
},
{
"service": "/svc/fuchsia.fonts.Provider",
"from": "realm",
"to": [
{ "dest": "#echo_server" },
]
},
{
"directory": "/data/assets",
"from": "self",
"to": [
{ "dest": "#echo_server" },
]
},
{
"directory": "/data/index",
"from": "realm",
"to": [
{ "dest": "#modular" },
]
},
],
"children": [
{
"name": "logger",
"url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
},
{
"name": "echo_server",
"url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo_server.cm"
}
],
"collections": [
{
"name": "modular",
"durability": "persistent",
},
]
}),
result = Ok(()),
},
test_cml_offer_all_valid_chars => {
input = json!({
"offer": [
{
"service": "/svc/fuchsia.logger.Log",
"from": "#abcdefghijklmnopqrstuvwxyz0123456789_-from",
"to": [
{
"dest": "#abcdefghijklmnopqrstuvwxyz0123456789_-to",
},
],
}
],
"children": [
{
"name": "abcdefghijklmnopqrstuvwxyz0123456789_-from",
"url": "https://www.google.com/gmail"
},
{
"name": "abcdefghijklmnopqrstuvwxyz0123456789_-to",
"url": "https://www.google.com/gmail"
},
]
}),
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": [
{ "dest": "#echo_server" },
]
} ]
}),
result = Err(Error::validate("\"#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@",
"to": [
{ "dest": "#echo_server" },
]
} ]
}),
result = Err(Error::validate_schema(CML_SCHEMA, "Pattern condition is not met at /offer/0/from")),
},
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_target_missing_props => {
input = json!({
"offer": [ {
"service": "/svc/fuchsia.logger.Log",
"from": "#logger",
"to": [
{ "as": "/svc/fuchsia.logger.SysLog" }
]
} ]
}),
result = Err(Error::validate_schema(CML_SCHEMA, "This property is required at /offer/0/to/0/dest")),
},
test_cml_offer_target_missing_to => {
input = json!({
"offer": [ {
"service": "/snvc/fuchsia.logger.Log",
"from": "#logger",
"to": [
{ "dest": "#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": [
{ "dest": "self", "as": "/svc/fuchsia.logger.SysLog" }
]
} ]
}),
result = Err(Error::validate_schema(CML_SCHEMA, "Pattern condition is not met at /offer/0/to/0/dest")),
},
test_cml_offer_target_equals_from => {
input = json!({
"offer": [ {
"service": "/svc/fuchsia.logger.Log",
"from": "#logger",
"to": [
{ "dest": "#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_offer_duplicate_target_paths => {
input = json!({
"offer": [
{
"service": "/svc/logger",
"from": "self",
"to": [
{ "dest": "#echo_server", "as": "/thing" },
{ "dest": "#scenic" }
]
},
{
"directory": "/thing",
"from": "realm",
"to": [
{ "dest": "#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\"")),
},
// 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("Duplicate child name: \"logger\"")),
},
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("Duplicate collection name: \"modular\"")),
},
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")),
},
// 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_path => {
input = json!({
"use": [
{ "directory": "/foo/?!@#$%/Bar" },
]
}),
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_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(()),
},
}
// 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" : ["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",
)),
)
}
}