blob: 7ebc225ec975d6bf4359bb41258bd5a32657db31 [file] [log] [blame]
// Copyright 2019 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 cm_json::{self, cm, Error, CM_SCHEMA};
use crate::validate;
use crate::cml;
use serde::ser::Serialize;
use serde_json::ser::{CompactFormatter, PrettyFormatter, Serializer};
use serde_json;
use std::fs;
use std::io::{Read, Write};
use std::path::PathBuf;
use std::str::from_utf8;
/// Read in a CML file and produce the equivalent CM.
pub fn compile(file: &PathBuf, pretty: bool, output: Option<PathBuf>) -> Result<(), Error> {
const BAD_EXTENSION: &str = "Input file does not have the component manifest extension (.cm)";
if let Some(ref path) = output {
match path.extension() {
Some(ext) => match ext.to_str() {
Some("cm") => Ok(()),
_ => Err(Error::invalid_args(BAD_EXTENSION)),
},
None => Err(Error::invalid_args(BAD_EXTENSION)),
}?;
}
let mut buffer = String::new();
fs::File::open(&file)?.read_to_string(&mut buffer)?;
let document = validate::validate_cml(&buffer)?;
let out = compile_cml(document)?;
let mut res = Vec::new();
if pretty {
let mut ser = Serializer::with_formatter(&mut res, PrettyFormatter::with_indent(b" "));
out.serialize(&mut ser)
.map_err(|e| Error::parse(format!("Couldn't serialize JSON: {}", e)))?;
} else {
let mut ser = Serializer::with_formatter(&mut res, CompactFormatter {});
out.serialize(&mut ser)
.map_err(|e| Error::parse(format!("Couldn't serialize JSON: {}", e)))?;
}
if let Some(output_path) = output {
fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(output_path)?
.write_all(&res)?;
} else {
println!("{}", from_utf8(&res)?);
}
// Sanity check that output conforms to CM schema.
let json = serde_json::from_slice(&res)
.map_err(|e| Error::parse(format!("Couldn't read output as JSON: {}", e)))?;
cm_json::validate_json(&json, CM_SCHEMA)?;
Ok(())
}
fn compile_cml(document: cml::Document) -> Result<cm::Document, Error> {
let mut out = cm::Document::default();
if let Some(program) = document.program {
out.program = Some(program.clone());
}
if let Some(r#use) = document.r#use {
out.uses = Some(translate_use(&r#use)?);
}
if let Some(expose) = document.expose {
out.exposes = Some(translate_expose(&expose)?);
}
if let Some(offer) = document.offer {
out.offers = Some(translate_offer(&offer)?);
}
if let Some(children) = document.children {
out.children = Some(translate_children(&children)?);
}
if let Some(facets) = document.facets {
out.facets = Some(facets.clone());
}
Ok(out)
}
fn translate_use(use_in: &Vec<cml::Use>) -> Result<Vec<cm::Use>, Error> {
let mut out_uses = vec![];
for use_ in use_in {
let (r#type, source_path) = extract_source_capability(use_)?;
let target_path = extract_target_path(use_, &source_path);
out_uses.push(cm::Use {
r#type,
source_path,
target_path,
});
}
Ok(out_uses)
}
fn translate_expose(expose_in: &Vec<cml::Expose>) -> Result<Vec<cm::Expose>, Error> {
let mut out_exposes = vec![];
for expose in expose_in.iter() {
let (r#type, source_path) = extract_source_capability(expose)?;
let source = extract_source(expose)?;
let target_path = extract_target_path(expose, &source_path);
out_exposes.push(cm::Expose {
r#type,
source_path,
source,
target_path,
});
}
Ok(out_exposes)
}
fn translate_offer(offer_in: &Vec<cml::Offer>) -> Result<Vec<cm::Offer>, Error> {
let mut out_offers = vec![];
for offer in offer_in.iter() {
let (r#type, source_path) = extract_source_capability(offer)?;
let source = extract_source(offer)?;
let targets = extract_targets(offer, &source_path)?;
out_offers.push(cm::Offer {
r#type,
source_path,
source,
targets,
});
}
Ok(out_offers)
}
fn translate_children(children_in: &Vec<cml::Child>) -> Result<Vec<cm::Child>, Error> {
let mut out_children = vec![];
for child in children_in.iter() {
out_children.push(cm::Child {
name: child.name.clone(),
uri: child.uri.clone(),
});
}
Ok(out_children)
}
// Extract "source" from "from".
fn extract_source<T>(in_obj: &T) -> Result<cm::Source, Error>
where
T: cml::FromClause,
{
let from = in_obj.from().to_string();
if !cml::FROM_RE.is_match(&from) {
return Err(Error::internal(format!("invalid \"from\": {}", from)));
}
let ret = if from.starts_with("#") {
let (_, child_name) = from.split_at(1);
cm::Source {
relation: "child".to_string(),
child_name: Some(child_name.to_string()),
}
} else {
cm::Source {
relation: from,
child_name: None,
}
};
Ok(ret)
}
// Extract "targets" from "targets".
fn extract_targets(in_obj: &cml::Offer, source_path: &str) -> Result<Vec<cm::Target>, Error> {
let mut out_targets = vec![];
for target in in_obj.targets.iter() {
let target_path = extract_target_path(target, source_path);
let caps = match cml::CHILD_RE.captures(&target.to) {
Some(c) => Ok(c),
None => Err(Error::internal(format!("invalid \"to\": {}", target.to))),
}?;
let child_name = caps[1].to_string();
out_targets.push(cm::Target {
target_path,
child_name,
});
}
Ok(out_targets)
}
fn extract_source_capability<T>(in_obj: &T) -> Result<(String, String), Error>
where
T: cml::CapabilityClause,
{
let (capability, source_path) = if let Some(p) = in_obj.service() {
(cml::SERVICE.to_string(), p.clone())
} else if let Some(p) = in_obj.directory() {
(cml::DIRECTORY.to_string(), p.clone())
} else {
return Err(Error::internal(format!("no source path")));
};
Ok((capability, source_path))
}
fn extract_target_path<T>(in_obj: &T, source_path: &str) -> String
where
T: cml::AsClause,
{
if let Some(as_) = in_obj.r#as() {
as_.clone()
} else {
source_path.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::fs::File;
use std::io;
use std::io::{Read, Write};
use tempfile::TempDir;
macro_rules! test_compile {
(
$(
$test_name:ident => {
input = $input:expr,
output = $result:expr,
},
)+
) => {
$(
#[test]
fn $test_name() {
compile_test($input, $result, true);
}
)+
}
}
fn compile_test(input: serde_json::value::Value, expected_output: &str, pretty: bool) {
let tmp_dir = TempDir::new().unwrap();
let tmp_in_path = tmp_dir.path().join("test.cml");
let tmp_out_path = tmp_dir.path().join("test.cm");
File::create(&tmp_in_path)
.unwrap()
.write_all(format!("{}", input).as_bytes())
.unwrap();
compile(&tmp_in_path, pretty, Some(tmp_out_path.clone())).expect("compilation failed");
let mut buffer = String::new();
fs::File::open(&tmp_out_path)
.unwrap()
.read_to_string(&mut buffer)
.unwrap();
assert_eq!(buffer, expected_output);
}
// TODO: Consider converting these to a golden test
test_compile! {
test_compile_empty => {
input = json!({}),
output = "{}",
},
test_compile_program => {
input = json!({
"program": {
"binary": "bin/app"
}
}),
output = r#"{
"program": {
"binary": "bin/app"
}
}"#,
},
test_compile_use => {
input = json!({
"use": [
{ "service": "/fonts/CoolFonts", "as": "/svc/fuchsia.fonts.Provider" },
{ "directory": "/data/assets" }
]
}),
output = r#"{
"uses": [
{
"type": "service",
"source_path": "/fonts/CoolFonts",
"target_path": "/svc/fuchsia.fonts.Provider"
},
{
"type": "directory",
"source_path": "/data/assets",
"target_path": "/data/assets"
}
]
}"#,
},
test_compile_expose => {
input = json!({
"expose": [
{
"service": "/loggers/fuchsia.logger.Log",
"from": "#logger",
"as": "/svc/fuchsia.logger.Log"
},
{ "directory": "/volumes/blobfs", "from": "self" }
],
"children": [
{
"name": "logger",
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
},
]
}),
output = r#"{
"exposes": [
{
"type": "service",
"source_path": "/loggers/fuchsia.logger.Log",
"source": {
"relation": "child",
"child_name": "logger"
},
"target_path": "/svc/fuchsia.logger.Log"
},
{
"type": "directory",
"source_path": "/volumes/blobfs",
"source": {
"relation": "self"
},
"target_path": "/volumes/blobfs"
}
],
"children": [
{
"name": "logger",
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
}
]
}"#,
},
test_compile_offer => {
input = json!({
"offer": [
{
"service": "/svc/fuchsia.logger.Log",
"from": "#logger",
"targets": [
{ "to": "#netstack" },
{ "to": "#echo2_server", "as": "/svc/fuchsia.logger.SysLog" }
]
},
{
"directory": "/data/assets",
"from": "realm",
"targets": [
{ "to": "#echo2_server" },
]
}
],
"children": [
{
"name": "logger",
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
},
{
"name": "echo2_server",
"uri": "fuchsia-pkg://fuchsia.com/echo2_server/stable#meta/echo2_server.cm"
},
{
"name": "netstack",
"uri": "fuchsia-pkg://fuchsia.com/netstack/stable#meta/netstack.cm"
}
]
}),
output = r#"{
"offers": [
{
"type": "service",
"source_path": "/svc/fuchsia.logger.Log",
"source": {
"relation": "child",
"child_name": "logger"
},
"targets": [
{
"target_path": "/svc/fuchsia.logger.Log",
"child_name": "netstack"
},
{
"target_path": "/svc/fuchsia.logger.SysLog",
"child_name": "echo2_server"
}
]
},
{
"type": "directory",
"source_path": "/data/assets",
"source": {
"relation": "realm"
},
"targets": [
{
"target_path": "/data/assets",
"child_name": "echo2_server"
}
]
}
],
"children": [
{
"name": "logger",
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
},
{
"name": "echo2_server",
"uri": "fuchsia-pkg://fuchsia.com/echo2_server/stable#meta/echo2_server.cm"
},
{
"name": "netstack",
"uri": "fuchsia-pkg://fuchsia.com/netstack/stable#meta/netstack.cm"
}
]
}"#,
},
test_compile_children => {
input = json!({
"children": [
{
"name": "logger",
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
},
{
"name": "gmail",
"uri": "https://www.google.com/gmail"
}
]
}),
output = r#"{
"children": [
{
"name": "logger",
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
},
{
"name": "gmail",
"uri": "https://www.google.com/gmail"
}
]
}"#,
},
// TODO(CF-167): JSON5 int->float parse bug
test_compile_facets => {
input = json!({
"facets": {
"metadata": {
"title": "foo",
"authors": [ "me", "you" ],
"year": 2018
}
}
}),
output = r#"{
"facets": {
"metadata": {
"authors": [
"me",
"you"
],
"title": "foo",
"year": 2018.0
}
}
}"#,
},
test_compile_all_sections => {
input = json!({
"program": {
"binary": "bin/app"
},
"use": [
{ "service": "/fonts/CoolFonts", "as": "/svc/fuchsia.fonts.Provider" },
],
"expose": [
{ "directory": "/volumes/blobfs", "from": "self" }
],
"offer": [
{
"service": "/svc/fuchsia.logger.Log",
"from": "#logger",
"targets": [
{ "to": "#netstack" }
]
}
],
"children": [
{
"name": "logger",
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
},
{
"name": "netstack",
"uri": "fuchsia-pkg://fuchsia.com/netstack/stable#meta/netstack.cm"
}
],
"facets": {
"author": "Fuchsia",
"year": 2018
}
}),
// TODO(CF-167): JSON5 int->float parse bug
output = r#"{
"program": {
"binary": "bin/app"
},
"uses": [
{
"type": "service",
"source_path": "/fonts/CoolFonts",
"target_path": "/svc/fuchsia.fonts.Provider"
}
],
"exposes": [
{
"type": "directory",
"source_path": "/volumes/blobfs",
"source": {
"relation": "self"
},
"target_path": "/volumes/blobfs"
}
],
"offers": [
{
"type": "service",
"source_path": "/svc/fuchsia.logger.Log",
"source": {
"relation": "child",
"child_name": "logger"
},
"targets": [
{
"target_path": "/svc/fuchsia.logger.Log",
"child_name": "netstack"
}
]
}
],
"children": [
{
"name": "logger",
"uri": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm"
},
{
"name": "netstack",
"uri": "fuchsia-pkg://fuchsia.com/netstack/stable#meta/netstack.cm"
}
],
"facets": {
"author": "Fuchsia",
"year": 2018.0
}
}"#,
},
}
#[test]
fn test_compile_compact() {
let input = json!({
"use": [
{ "service": "/fonts/CoolFonts", "as": "/svc/fuchsia.fonts.Provider" },
{ "directory": "/data/assets" }
]
});
let output = r#"{"uses":[{"type":"service","source_path":"/fonts/CoolFonts","target_path":"/svc/fuchsia.fonts.Provider"},{"type":"directory","source_path":"/data/assets","target_path":"/data/assets"}]}"#;
compile_test(input, &output, false);
}
#[test]
fn test_invalid_json() {
let tmp_dir = TempDir::new().unwrap();
let tmp_in_path = tmp_dir.path().join("test.cml");
let tmp_out_path = tmp_dir.path().join("test.cm");
let input = json!({
"expose": [
{ "directory": "/volumes/blobfs", "from": "realm" }
]
});
File::create(&tmp_in_path)
.unwrap()
.write_all(format!("{}", input).as_bytes())
.unwrap();
{
let result = compile(&tmp_in_path, false, Some(tmp_out_path.clone()));
let expected_result: Result<(), Error> = Err(Error::parse(
"Pattern condition is not met at /expose/0/from",
));
assert_eq!(format!("{:?}", result), format!("{:?}", expected_result));
}
// Compilation failed so output should not exist.
{
let result = fs::File::open(&tmp_out_path);
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
}
}
}