| // 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 crate::cml::{self, CapabilityClause}; |
| use crate::validate; |
| use cm_json::{self, cm, Error}; |
| use serde::ser::Serialize; |
| use serde_json; |
| use serde_json::ser::{CompactFormatter, PrettyFormatter, Serializer}; |
| use std::collections::HashSet; |
| use std::fs::{self, File}; |
| 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_IN_EXTENSION: &str = "Input file does not have the component manifest language \ |
| extension (.cml)"; |
| match file.extension().and_then(|e| e.to_str()) { |
| Some("cml") => Ok(()), |
| _ => Err(Error::invalid_args(BAD_IN_EXTENSION)), |
| }?; |
| const BAD_OUT_EXTENSION: &str = |
| "Output file does not have the component manifest extension (.cm)"; |
| if let Some(ref path) = output { |
| match path.extension().and_then(|e| e.to_str()) { |
| Some("cm") => Ok(()), |
| _ => Err(Error::invalid_args(BAD_OUT_EXTENSION)), |
| }?; |
| } |
| |
| let mut buffer = String::new(); |
| File::open(&file.as_path())?.read_to_string(&mut buffer)?; |
| let value = cm_json::from_json5_str(&buffer)?; |
| let document = validate::parse_cml(value)?; |
| 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. |
| serde_json::from_slice::<cm::Document>(&res) |
| .map_err(|e| Error::parse(format!("Couldn't read output as JSON: {}", e)))?; |
| Ok(()) |
| } |
| |
| fn compile_cml(document: cml::Document) -> Result<cm::Document, Error> { |
| let mut out = cm::Document::default(); |
| if let Some(program) = document.program.as_ref() { |
| out.program = Some(program.clone()); |
| } |
| if let Some(r#use) = document.r#use.as_ref() { |
| out.uses = Some(translate_use(r#use)?); |
| } |
| if let Some(expose) = document.expose.as_ref() { |
| out.exposes = Some(translate_expose(expose)?); |
| } |
| if let Some(offer) = document.offer.as_ref() { |
| let all_children = document.all_children_names().into_iter().collect(); |
| let all_collections = document.all_collection_names().into_iter().collect(); |
| out.offers = Some(translate_offer(offer, &all_children, &all_collections)?); |
| } |
| if let Some(children) = document.children.as_ref() { |
| out.children = Some(translate_children(children)?); |
| } |
| if let Some(collections) = document.collections.as_ref() { |
| out.collections = Some(translate_collections(collections)?); |
| } |
| if let Some(storage) = document.storage.as_ref() { |
| out.storage = Some(translate_storage(storage)?); |
| } |
| if let Some(facets) = document.facets.as_ref() { |
| 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 target_path = extract_target_path(use_, use_); |
| let out = if let Some(p) = use_.service() { |
| let source = extract_use_source(use_)?; |
| let target_path = target_path.ok_or(Error::internal(format!("no capability")))?; |
| Ok(cm::Use::Service(cm::UseService { |
| source, |
| source_path: cm::Path::new(p.clone())?, |
| target_path: cm::Path::new(target_path)?, |
| })) |
| } else if let Some(p) = use_.legacy_service() { |
| let source = extract_use_source(use_)?; |
| let target_path = target_path.ok_or(Error::internal(format!("no capability")))?; |
| Ok(cm::Use::LegacyService(cm::UseLegacyService { |
| source, |
| source_path: cm::Path::new(p.clone())?, |
| target_path: cm::Path::new(target_path)?, |
| })) |
| } else if let Some(p) = use_.directory() { |
| let source = extract_use_source(use_)?; |
| let target_path = target_path.ok_or(Error::internal(format!("no capability")))?; |
| Ok(cm::Use::Directory(cm::UseDirectory { |
| source, |
| source_path: cm::Path::new(p.clone())?, |
| target_path: cm::Path::new(target_path)?, |
| })) |
| } else if let Some(p) = use_.storage() { |
| Ok(cm::Use::Storage(cm::UseStorage { |
| type_: str_to_storage_type(p.as_str())?, |
| target_path: target_path.map(cm::Path::new).transpose()?, |
| })) |
| } else { |
| Err(Error::internal(format!("no capability"))) |
| }?; |
| out_uses.push(out); |
| } |
| 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 source = extract_expose_source(expose)?; |
| let target_path = |
| extract_target_path(expose, expose).ok_or(Error::internal(format!("no capability")))?; |
| let out = if let Some(p) = expose.service() { |
| Ok(cm::Expose::Service(cm::ExposeService { |
| source, |
| source_path: cm::Path::new(p.clone())?, |
| target_path: cm::Path::new(target_path)?, |
| })) |
| } else if let Some(p) = expose.legacy_service() { |
| Ok(cm::Expose::LegacyService(cm::ExposeLegacyService { |
| source, |
| source_path: cm::Path::new(p.clone())?, |
| target_path: cm::Path::new(target_path)?, |
| })) |
| } else if let Some(p) = expose.directory() { |
| Ok(cm::Expose::Directory(cm::ExposeDirectory { |
| source, |
| source_path: cm::Path::new(p.clone())?, |
| target_path: cm::Path::new(target_path)?, |
| })) |
| } else { |
| Err(Error::internal(format!("no capability"))) |
| }?; |
| out_exposes.push(out); |
| } |
| Ok(out_exposes) |
| } |
| |
| fn translate_offer( |
| offer_in: &Vec<cml::Offer>, |
| all_children: &HashSet<&str>, |
| all_collections: &HashSet<&str>, |
| ) -> Result<Vec<cm::Offer>, Error> { |
| let mut out_offers = vec![]; |
| for offer in offer_in.iter() { |
| if let Some(p) = offer.service() { |
| let source = extract_offer_source(offer)?; |
| let targets = extract_targets(offer, all_children, all_collections)?; |
| for (target, target_path) in targets { |
| out_offers.push(cm::Offer::Service(cm::OfferService { |
| source_path: cm::Path::new(p.clone())?, |
| source: source.clone(), |
| target, |
| target_path: cm::Path::new(target_path)?, |
| })); |
| } |
| } else if let Some(p) = offer.legacy_service() { |
| let source = extract_offer_source(offer)?; |
| let targets = extract_targets(offer, all_children, all_collections)?; |
| for (target, target_path) in targets { |
| out_offers.push(cm::Offer::LegacyService(cm::OfferLegacyService { |
| source_path: cm::Path::new(p.clone())?, |
| source: source.clone(), |
| target, |
| target_path: cm::Path::new(target_path)?, |
| })); |
| } |
| } else if let Some(p) = offer.directory() { |
| let source = extract_offer_source(offer)?; |
| let targets = extract_targets(offer, all_children, all_collections)?; |
| for (target, target_path) in targets { |
| out_offers.push(cm::Offer::Directory(cm::OfferDirectory { |
| source_path: cm::Path::new(p.clone())?, |
| source: source.clone(), |
| target, |
| target_path: cm::Path::new(target_path)?, |
| })); |
| } |
| } else if let Some(p) = offer.storage() { |
| let type_ = str_to_storage_type(p.as_str())?; |
| let source = extract_offer_storage_source(offer)?; |
| let targets = extract_storage_targets(offer, all_children, all_collections)?; |
| for target in targets { |
| out_offers.push(cm::Offer::Storage(cm::OfferStorage { |
| type_: type_.clone(), |
| source: source.clone(), |
| target, |
| })); |
| } |
| } else { |
| return Err(Error::internal(format!("no capability"))); |
| } |
| } |
| 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() { |
| let startup = match child.startup.as_ref().map(|s| s as &str) { |
| Some(cml::LAZY) | None => cm::StartupMode::Lazy, |
| Some(cml::EAGER) => cm::StartupMode::Eager, |
| Some(_) => { |
| return Err(Error::internal(format!("invalid startup"))); |
| } |
| }; |
| out_children.push(cm::Child { |
| name: cm::Name::new(child.name.clone())?, |
| url: cm::Url::new(child.url.clone())?, |
| startup, |
| }); |
| } |
| Ok(out_children) |
| } |
| |
| fn translate_collections( |
| collections_in: &Vec<cml::Collection>, |
| ) -> Result<Vec<cm::Collection>, Error> { |
| let mut out_collections = vec![]; |
| for collection in collections_in.iter() { |
| let durability = match &collection.durability as &str { |
| cml::PERSISTENT => cm::Durability::Persistent, |
| cml::TRANSIENT => cm::Durability::Transient, |
| _ => { |
| return Err(Error::internal(format!("invalid durability"))); |
| } |
| }; |
| out_collections |
| .push(cm::Collection { name: cm::Name::new(collection.name.clone())?, durability }); |
| } |
| Ok(out_collections) |
| } |
| |
| fn translate_storage(storage_in: &Vec<cml::Storage>) -> Result<Vec<cm::Storage>, Error> { |
| storage_in |
| .iter() |
| .map(|storage| { |
| Ok(cm::Storage { |
| name: cm::Name::new(storage.name.clone())?, |
| source_path: cm::Path::new(storage.path.clone())?, |
| source: extract_offer_source(storage)?, |
| }) |
| }) |
| .collect() |
| } |
| |
| fn extract_use_source(in_obj: &cml::Use) -> Result<cm::Ref, Error> { |
| match in_obj.from.as_ref() { |
| Some(from) => match from as &str { |
| "realm" => Ok(cm::Ref::Realm(cm::RealmRef {})), |
| "framework" => Ok(cm::Ref::Framework(cm::FrameworkRef {})), |
| _ => Err(Error::internal(format!("invalid \"from\" for \"use\": {}", from))), |
| }, |
| None => Ok(cm::Ref::Realm(cm::RealmRef {})), |
| } |
| } |
| |
| fn extract_expose_source<T>(in_obj: &T) -> Result<cm::Ref, 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::Ref::Child(cm::ChildRef { name: cm::Name::new(child_name.to_string())? }) |
| } else if from == "framework" { |
| cm::Ref::Framework(cm::FrameworkRef {}) |
| } else if from == "self" { |
| cm::Ref::Self_(cm::SelfRef {}) |
| } else { |
| return Err(Error::internal(format!("invalid \"from\" for \"expose\": {}", from))); |
| }; |
| Ok(ret) |
| } |
| |
| fn extract_offer_source<T>(in_obj: &T) -> Result<cm::Ref, 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::Ref::Child(cm::ChildRef { name: cm::Name::new(child_name.to_string())? }) |
| } else if from == "framework" { |
| cm::Ref::Framework(cm::FrameworkRef {}) |
| } else if from == "realm" { |
| cm::Ref::Realm(cm::RealmRef {}) |
| } else if from == "self" { |
| cm::Ref::Self_(cm::SelfRef {}) |
| } else { |
| return Err(Error::internal(format!("invalid \"from\" for \"offer\": {}", from))); |
| }; |
| Ok(ret) |
| } |
| |
| fn extract_offer_storage_source<T>(in_obj: &T) -> Result<cm::Ref, 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 (_, storage_name) = from.split_at(1); |
| cm::Ref::Storage(cm::StorageRef { name: cm::Name::new(storage_name.to_string())? }) |
| } else if from == "realm" { |
| cm::Ref::Realm(cm::RealmRef {}) |
| } else { |
| return Err(Error::internal(format!("invalid \"from\" for \"offer\": {}", from))); |
| }; |
| Ok(ret) |
| } |
| |
| fn extract_storage_targets( |
| in_obj: &cml::Offer, |
| all_children: &HashSet<&str>, |
| all_collections: &HashSet<&str>, |
| ) -> Result<Vec<cm::Ref>, Error> { |
| in_obj |
| .to |
| .iter() |
| .map(|to| { |
| let caps = match cml::REFERENCE_RE.captures(&to.dest) { |
| Some(c) => Ok(c), |
| None => Err(Error::internal(format!("invalid \"dest\": {}", to.dest))), |
| }?; |
| let name = caps[1].to_string(); |
| |
| if all_children.contains(&name as &str) { |
| Ok(cm::Ref::Child(cm::ChildRef { name: cm::Name::new(name.to_string())? })) |
| } else if all_collections.contains(&name as &str) { |
| Ok(cm::Ref::Collection(cm::CollectionRef { |
| name: cm::Name::new(name.to_string())?, |
| })) |
| } else { |
| Err(Error::internal(format!("dangling reference: \"{}\"", name))) |
| } |
| }) |
| .collect() |
| } |
| |
| fn extract_targets( |
| in_obj: &cml::Offer, |
| all_children: &HashSet<&str>, |
| all_collections: &HashSet<&str>, |
| ) -> Result<Vec<(cm::Ref, String)>, Error> { |
| let mut out_targets = vec![]; |
| for to in in_obj.to.iter() { |
| let target_path = |
| extract_target_path(in_obj, to).ok_or(Error::internal("no capability".to_string()))?; |
| let caps = match cml::REFERENCE_RE.captures(&to.dest) { |
| Some(c) => Ok(c), |
| None => Err(Error::internal(format!("invalid \"dest\": {}", to.dest))), |
| }?; |
| let name = caps[1].to_string(); |
| let target = if all_children.contains(&name as &str) { |
| cm::Ref::Child(cm::ChildRef { name: cm::Name::new(name.to_string())? }) |
| } else if all_collections.contains(&name as &str) { |
| cm::Ref::Collection(cm::CollectionRef { name: cm::Name::new(name.to_string())? }) |
| } else { |
| return Err(Error::internal(format!("dangling reference: \"{}\"", name))); |
| }; |
| out_targets.push((target, target_path)); |
| } |
| Ok(out_targets) |
| } |
| |
| fn extract_target_path<T, U>(in_obj: &T, to_obj: &U) -> Option<String> |
| where |
| T: cml::CapabilityClause, |
| U: cml::AsClause, |
| { |
| if let Some(as_) = to_obj.r#as() { |
| Some(as_.clone()) |
| } else { |
| if let Some(p) = in_obj.service() { |
| Some(p.clone()) |
| } else if let Some(p) = in_obj.legacy_service() { |
| Some(p.clone()) |
| } else if let Some(p) = in_obj.directory() { |
| Some(p.clone()) |
| } else if let Some(type_) = in_obj.storage() { |
| match type_.as_str() { |
| "data" => Some("/data".to_string()), |
| "cache" => Some("/cache".to_string()), |
| _ => None, |
| } |
| } else { |
| None |
| } |
| } |
| } |
| |
| fn str_to_storage_type(s: &str) -> Result<cm::StorageType, Error> { |
| match s { |
| "data" => Ok(cm::StorageType::Data), |
| "cache" => Ok(cm::StorageType::Cache), |
| "meta" => Ok(cm::StorageType::Meta), |
| t => Err(Error::internal(format!("unknown storage type: {}", t))), |
| } |
| } |
| |
| #[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 { |
| ( |
| $( |
| $(#[$m:meta])* |
| $test_name:ident => { |
| input = $input:expr, |
| output = $result:expr, |
| }, |
| )+ |
| ) => { |
| $( |
| $(#[$m])* |
| #[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" }, |
| { "service": "/svc/fuchsia.sys2.Realm", "from": "framework" }, |
| { "legacy_service": "/fonts/LegacyCoolFonts", "as": "/svc/fuchsia.fonts.LegacyProvider" }, |
| { "legacy_service": "/svc/fuchsia.sys2.LegacyRealm", "from": "framework" }, |
| { "directory": "/data/assets" }, |
| { "directory": "/data/config", "from": "realm" }, |
| { "storage": "meta" }, |
| { "storage": "cache", "as": "/tmp" }, |
| ], |
| }), |
| output = r#"{ |
| "uses": [ |
| { |
| "service": { |
| "source": { |
| "realm": {} |
| }, |
| "source_path": "/fonts/CoolFonts", |
| "target_path": "/svc/fuchsia.fonts.Provider" |
| } |
| }, |
| { |
| "service": { |
| "source": { |
| "framework": {} |
| }, |
| "source_path": "/svc/fuchsia.sys2.Realm", |
| "target_path": "/svc/fuchsia.sys2.Realm" |
| } |
| }, |
| { |
| "legacy_service": { |
| "source": { |
| "realm": {} |
| }, |
| "source_path": "/fonts/LegacyCoolFonts", |
| "target_path": "/svc/fuchsia.fonts.LegacyProvider" |
| } |
| }, |
| { |
| "legacy_service": { |
| "source": { |
| "framework": {} |
| }, |
| "source_path": "/svc/fuchsia.sys2.LegacyRealm", |
| "target_path": "/svc/fuchsia.sys2.LegacyRealm" |
| } |
| }, |
| { |
| "directory": { |
| "source": { |
| "realm": {} |
| }, |
| "source_path": "/data/assets", |
| "target_path": "/data/assets" |
| } |
| }, |
| { |
| "directory": { |
| "source": { |
| "realm": {} |
| }, |
| "source_path": "/data/config", |
| "target_path": "/data/config" |
| } |
| }, |
| { |
| "storage": { |
| "type": "meta" |
| } |
| }, |
| { |
| "storage": { |
| "type": "cache", |
| "target_path": "/tmp" |
| } |
| } |
| ] |
| }"#, |
| }, |
| |
| test_compile_expose => { |
| input = json!({ |
| "expose": [ |
| { |
| "service": "/loggers/fuchsia.logger.Log", |
| "from": "#logger", |
| "as": "/svc/fuchsia.logger.Log" |
| }, |
| { |
| "legacy_service": "/loggers/fuchsia.logger.LegacyLog", |
| "from": "#logger", |
| "as": "/svc/fuchsia.logger.LegacyLog" |
| }, |
| { "directory": "/volumes/blobfs", "from": "self" }, |
| { "directory": "/hub", "from": "framework" } |
| ], |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm" |
| }, |
| ] |
| }), |
| output = r#"{ |
| "exposes": [ |
| { |
| "service": { |
| "source": { |
| "child": { |
| "name": "logger" |
| } |
| }, |
| "source_path": "/loggers/fuchsia.logger.Log", |
| "target_path": "/svc/fuchsia.logger.Log" |
| } |
| }, |
| { |
| "legacy_service": { |
| "source": { |
| "child": { |
| "name": "logger" |
| } |
| }, |
| "source_path": "/loggers/fuchsia.logger.LegacyLog", |
| "target_path": "/svc/fuchsia.logger.LegacyLog" |
| } |
| }, |
| { |
| "directory": { |
| "source": { |
| "self": {} |
| }, |
| "source_path": "/volumes/blobfs", |
| "target_path": "/volumes/blobfs" |
| } |
| }, |
| { |
| "directory": { |
| "source": { |
| "framework": {} |
| }, |
| "source_path": "/hub", |
| "target_path": "/hub" |
| } |
| } |
| ], |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| "startup": "lazy" |
| } |
| ] |
| }"#, |
| }, |
| |
| test_compile_offer => { |
| input = json!({ |
| "offer": [ |
| { |
| "service": "/svc/fuchsia.logger.Log", |
| "from": "#logger", |
| "to": [ |
| { "dest": "#netstack" }, |
| { "dest": "#modular", "as": "/svc/fuchsia.logger.SysLog" }, |
| ] |
| }, |
| { |
| "legacy_service": "/svc/fuchsia.logger.LegacyLog", |
| "from": "#logger", |
| "to": [ |
| { "dest": "#netstack" }, |
| { "dest": "#modular", "as": "/svc/fuchsia.logger.LegacySysLog" }, |
| ] |
| }, |
| { |
| "directory": "/data/assets", |
| "from": "realm", |
| "to": [ |
| { "dest": "#netstack" }, |
| { "dest": "#modular", "as": "/data" } |
| ] |
| }, |
| { |
| "directory": "/hub", |
| "from": "framework", |
| "to": [ |
| { "dest": "#modular", "as": "/hub" } |
| ] |
| }, |
| { |
| "storage": "data", |
| "from": "#logger-storage", |
| "to": [ |
| { "dest": "#netstack" }, |
| { "dest": "#modular" } |
| ] |
| }, |
| ], |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm" |
| }, |
| { |
| "name": "netstack", |
| "url": "fuchsia-pkg://fuchsia.com/netstack/stable#meta/netstack.cm" |
| }, |
| ], |
| "collections": [ |
| { |
| "name": "modular", |
| "durability": "persistent", |
| }, |
| ], |
| "storage": [ |
| { |
| "name": "logger-storage", |
| "path": "/minfs", |
| "from": "#logger", |
| }, |
| ], |
| }), |
| output = r#"{ |
| "offers": [ |
| { |
| "service": { |
| "source": { |
| "child": { |
| "name": "logger" |
| } |
| }, |
| "source_path": "/svc/fuchsia.logger.Log", |
| "target": { |
| "child": { |
| "name": "netstack" |
| } |
| }, |
| "target_path": "/svc/fuchsia.logger.Log" |
| } |
| }, |
| { |
| "service": { |
| "source": { |
| "child": { |
| "name": "logger" |
| } |
| }, |
| "source_path": "/svc/fuchsia.logger.Log", |
| "target": { |
| "collection": { |
| "name": "modular" |
| } |
| }, |
| "target_path": "/svc/fuchsia.logger.SysLog" |
| } |
| }, |
| { |
| "legacy_service": { |
| "source": { |
| "child": { |
| "name": "logger" |
| } |
| }, |
| "source_path": "/svc/fuchsia.logger.LegacyLog", |
| "target": { |
| "child": { |
| "name": "netstack" |
| } |
| }, |
| "target_path": "/svc/fuchsia.logger.LegacyLog" |
| } |
| }, |
| { |
| "legacy_service": { |
| "source": { |
| "child": { |
| "name": "logger" |
| } |
| }, |
| "source_path": "/svc/fuchsia.logger.LegacyLog", |
| "target": { |
| "collection": { |
| "name": "modular" |
| } |
| }, |
| "target_path": "/svc/fuchsia.logger.LegacySysLog" |
| } |
| }, |
| { |
| "directory": { |
| "source": { |
| "realm": {} |
| }, |
| "source_path": "/data/assets", |
| "target": { |
| "child": { |
| "name": "netstack" |
| } |
| }, |
| "target_path": "/data/assets" |
| } |
| }, |
| { |
| "directory": { |
| "source": { |
| "realm": {} |
| }, |
| "source_path": "/data/assets", |
| "target": { |
| "collection": { |
| "name": "modular" |
| } |
| }, |
| "target_path": "/data" |
| } |
| }, |
| { |
| "directory": { |
| "source": { |
| "framework": {} |
| }, |
| "source_path": "/hub", |
| "target": { |
| "collection": { |
| "name": "modular" |
| } |
| }, |
| "target_path": "/hub" |
| } |
| }, |
| { |
| "storage": { |
| "type": "data", |
| "source": { |
| "storage": { |
| "name": "logger-storage" |
| } |
| }, |
| "target": { |
| "child": { |
| "name": "netstack" |
| } |
| } |
| } |
| }, |
| { |
| "storage": { |
| "type": "data", |
| "source": { |
| "storage": { |
| "name": "logger-storage" |
| } |
| }, |
| "target": { |
| "collection": { |
| "name": "modular" |
| } |
| } |
| } |
| } |
| ], |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| "startup": "lazy" |
| }, |
| { |
| "name": "netstack", |
| "url": "fuchsia-pkg://fuchsia.com/netstack/stable#meta/netstack.cm", |
| "startup": "lazy" |
| } |
| ], |
| "collections": [ |
| { |
| "name": "modular", |
| "durability": "persistent" |
| } |
| ], |
| "storage": [ |
| { |
| "name": "logger-storage", |
| "source_path": "/minfs", |
| "source": { |
| "child": { |
| "name": "logger" |
| } |
| } |
| } |
| ] |
| }"#, |
| }, |
| |
| test_compile_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", |
| }, |
| ] |
| }), |
| output = r#"{ |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| "startup": "lazy" |
| }, |
| { |
| "name": "gmail", |
| "url": "https://www.google.com/gmail", |
| "startup": "eager" |
| }, |
| { |
| "name": "echo", |
| "url": "fuchsia-pkg://fuchsia.com/echo/stable#meta/echo.cm", |
| "startup": "lazy" |
| } |
| ] |
| }"#, |
| }, |
| |
| test_compile_collections => { |
| input = json!({ |
| "collections": [ |
| { |
| "name": "modular", |
| "durability": "persistent", |
| }, |
| { |
| "name": "tests", |
| "durability": "transient", |
| }, |
| ] |
| }), |
| output = r#"{ |
| "collections": [ |
| { |
| "name": "modular", |
| "durability": "persistent" |
| }, |
| { |
| "name": "tests", |
| "durability": "transient" |
| } |
| ] |
| }"#, |
| }, |
| |
| test_compile_storage => { |
| input = json!({ |
| "storage": [ |
| { |
| "name": "mystorage", |
| "path": "/storage", |
| "from": "#minfs", |
| } |
| ], |
| "children": [ |
| { |
| "name": "minfs", |
| "url": "fuchsia-pkg://fuchsia.com/minfs/stable#meta/minfs.cm", |
| }, |
| ] |
| }), |
| output = r#"{ |
| "children": [ |
| { |
| "name": "minfs", |
| "url": "fuchsia-pkg://fuchsia.com/minfs/stable#meta/minfs.cm", |
| "startup": "lazy" |
| } |
| ], |
| "storage": [ |
| { |
| "name": "mystorage", |
| "source_path": "/storage", |
| "source": { |
| "child": { |
| "name": "minfs" |
| } |
| } |
| } |
| ] |
| }"#, |
| }, |
| |
| 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 |
| } |
| } |
| }"#, |
| }, |
| |
| test_compile_all_sections => { |
| input = json!({ |
| "program": { |
| "binary": "bin/app", |
| }, |
| "use": [ |
| { "service": "/fonts/CoolFonts", "as": "/svc/fuchsia.fonts.Provider" }, |
| { "legacy_service": "/fonts/LegacyCoolFonts", "as": "/svc/fuchsia.fonts.LegacyProvider" }, |
| ], |
| "expose": [ |
| { "directory": "/volumes/blobfs", "from": "self" }, |
| ], |
| "offer": [ |
| { |
| "service": "/svc/fuchsia.logger.Log", |
| "from": "#logger", |
| "to": [ |
| { "dest": "#netstack" }, |
| { "dest": "#modular" }, |
| ], |
| }, |
| { |
| "legacy_service": "/svc/fuchsia.logger.LegacyLog", |
| "from": "#logger", |
| "to": [ |
| { "dest": "#netstack" }, |
| { "dest": "#modular" }, |
| ], |
| }, |
| ], |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| }, |
| { |
| "name": "netstack", |
| "url": "fuchsia-pkg://fuchsia.com/netstack/stable#meta/netstack.cm", |
| }, |
| ], |
| "collections": [ |
| { |
| "name": "modular", |
| "durability": "persistent", |
| }, |
| ], |
| "facets": { |
| "author": "Fuchsia", |
| "year": 2018, |
| }, |
| }), |
| output = r#"{ |
| "program": { |
| "binary": "bin/app" |
| }, |
| "uses": [ |
| { |
| "service": { |
| "source": { |
| "realm": {} |
| }, |
| "source_path": "/fonts/CoolFonts", |
| "target_path": "/svc/fuchsia.fonts.Provider" |
| } |
| }, |
| { |
| "legacy_service": { |
| "source": { |
| "realm": {} |
| }, |
| "source_path": "/fonts/LegacyCoolFonts", |
| "target_path": "/svc/fuchsia.fonts.LegacyProvider" |
| } |
| } |
| ], |
| "exposes": [ |
| { |
| "directory": { |
| "source": { |
| "self": {} |
| }, |
| "source_path": "/volumes/blobfs", |
| "target_path": "/volumes/blobfs" |
| } |
| } |
| ], |
| "offers": [ |
| { |
| "service": { |
| "source": { |
| "child": { |
| "name": "logger" |
| } |
| }, |
| "source_path": "/svc/fuchsia.logger.Log", |
| "target": { |
| "child": { |
| "name": "netstack" |
| } |
| }, |
| "target_path": "/svc/fuchsia.logger.Log" |
| } |
| }, |
| { |
| "service": { |
| "source": { |
| "child": { |
| "name": "logger" |
| } |
| }, |
| "source_path": "/svc/fuchsia.logger.Log", |
| "target": { |
| "collection": { |
| "name": "modular" |
| } |
| }, |
| "target_path": "/svc/fuchsia.logger.Log" |
| } |
| }, |
| { |
| "legacy_service": { |
| "source": { |
| "child": { |
| "name": "logger" |
| } |
| }, |
| "source_path": "/svc/fuchsia.logger.LegacyLog", |
| "target": { |
| "child": { |
| "name": "netstack" |
| } |
| }, |
| "target_path": "/svc/fuchsia.logger.LegacyLog" |
| } |
| }, |
| { |
| "legacy_service": { |
| "source": { |
| "child": { |
| "name": "logger" |
| } |
| }, |
| "source_path": "/svc/fuchsia.logger.LegacyLog", |
| "target": { |
| "collection": { |
| "name": "modular" |
| } |
| }, |
| "target_path": "/svc/fuchsia.logger.LegacyLog" |
| } |
| } |
| ], |
| "children": [ |
| { |
| "name": "logger", |
| "url": "fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm", |
| "startup": "lazy" |
| }, |
| { |
| "name": "netstack", |
| "url": "fuchsia-pkg://fuchsia.com/netstack/stable#meta/netstack.cm", |
| "startup": "lazy" |
| } |
| ], |
| "collections": [ |
| { |
| "name": "modular", |
| "durability": "persistent" |
| } |
| ], |
| "facets": { |
| "author": "Fuchsia", |
| "year": 2018 |
| } |
| }"#, |
| }, |
| } |
| |
| #[test] |
| fn test_compile_compact() { |
| let input = json!({ |
| "use": [ |
| { "service": "/fonts/CoolFonts", "as": "/svc/fuchsia.fonts.Provider" }, |
| { "legacy_service": "/fonts/LegacyCoolFonts", "as": "/svc/fuchsia.fonts.LegacyProvider" }, |
| { "directory": "/data/assets" } |
| ] |
| }); |
| let output = r#"{"uses":[{"service":{"source":{"realm":{}},"source_path":"/fonts/CoolFonts","target_path":"/svc/fuchsia.fonts.Provider"}},{"legacy_service":{"source":{"realm":{}},"source_path":"/fonts/LegacyCoolFonts","target_path":"/svc/fuchsia.fonts.LegacyProvider"}},{"directory":{"source":{"realm":{}},"source_path":"/data/assets","target_path":"/data/assets"}}]}"#; |
| compile_test(input, &output, false); |
| } |
| |
| #[test] |
| fn test_invalid_json() { |
| use cm_json::CML_SCHEMA; |
| |
| 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::validate_schema( |
| CML_SCHEMA, |
| "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); |
| } |
| } |
| } |