| // 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}; |
| use fidl_fuchsia_data as fd; |
| use fidl_fuchsia_sys2::{ |
| CapabilityType, ChildDecl, ComponentDecl, ExposeDecl, OfferDecl, OfferTarget, Relation, |
| RelativeId, UseDecl, |
| }; |
| use serde_json::{Map, Value}; |
| |
| // Read in a CML file and produce the equivalent FIDL. |
| pub fn translate(buffer: &str) -> Result<ComponentDecl, Error> { |
| let json = cm_json::from_json_str(&buffer)?; |
| cm_json::validate_json(&json, cm_json::CM_SCHEMA)?; |
| let document: cm::Document = serde_json::from_str(&buffer) |
| .map_err(|e| Error::parse(format!("Couldn't read input as struct: {}", e)))?; |
| translate_cm(document) |
| } |
| |
| fn translate_cm(document: cm::Document) -> Result<ComponentDecl, Error> { |
| // We can't deserialize into a ComponentDecl fidl directly because the autogenerated rust |
| // bindings do not contain the necessary attributes to deserialize correctly. Also, the |
| // deserialization of freeform objects to dictionaries is not trivial. |
| let mut out = new_component_decl(); |
| if let Some(program) = document.program { |
| out.program = Some(dictionary_from_map(program)?); |
| } |
| if let Some(uses) = document.uses { |
| out.uses = Some(translate_uses(uses)?); |
| } |
| if let Some(exposes) = document.exposes { |
| out.exposes = Some(translate_exposes(exposes)?); |
| } |
| if let Some(offers) = document.offers { |
| out.offers = Some(translate_offers(offers)?); |
| } |
| if let Some(children) = document.children { |
| out.children = Some(translate_children(children)?); |
| } |
| if let Some(facets) = document.facets { |
| out.facets = Some(dictionary_from_map(facets)?); |
| } |
| Ok(out) |
| } |
| |
| fn translate_uses(use_in: Vec<cm::Use>) -> Result<Vec<UseDecl>, Error> { |
| let mut out_uses = vec![]; |
| for use_ in use_in { |
| let type_ = capability_from_str(&use_.r#type)?; |
| out_uses.push(UseDecl { |
| type_: Some(type_), |
| source_path: Some(use_.source_path), |
| target_path: Some(use_.target_path), |
| }); |
| } |
| Ok(out_uses) |
| } |
| |
| fn translate_exposes(expose_in: Vec<cm::Expose>) -> Result<Vec<ExposeDecl>, Error> { |
| let mut out_exposes = vec![]; |
| for expose in expose_in { |
| let type_ = capability_from_str(&expose.r#type)?; |
| let source = RelativeId { |
| relation: Some(relation_from_str(&expose.source.relation)?), |
| child_name: expose.source.child_name, |
| }; |
| out_exposes.push(ExposeDecl { |
| type_: Some(type_), |
| source_path: Some(expose.source_path), |
| source: Some(source), |
| target_path: Some(expose.target_path), |
| }); |
| } |
| Ok(out_exposes) |
| } |
| |
| fn translate_offers(offer_in: Vec<cm::Offer>) -> Result<Vec<OfferDecl>, Error> { |
| let mut out_offers = vec![]; |
| for offer in offer_in { |
| let type_ = capability_from_str(&offer.r#type)?; |
| let source = RelativeId { |
| relation: Some(relation_from_str(&offer.source.relation)?), |
| child_name: offer.source.child_name, |
| }; |
| let mut out_targets = vec![]; |
| for target in offer.targets { |
| out_targets.push(OfferTarget { |
| target_path: Some(target.target_path), |
| child_name: Some(target.child_name), |
| }); |
| } |
| out_offers.push(OfferDecl { |
| type_: Some(type_), |
| source_path: Some(offer.source_path), |
| source: Some(source), |
| targets: Some(out_targets), |
| }); |
| } |
| Ok(out_offers) |
| } |
| |
| fn translate_children(children_in: Vec<cm::Child>) -> Result<Vec<ChildDecl>, Error> { |
| let mut out_children = vec![]; |
| for child in children_in { |
| out_children.push(ChildDecl { |
| name: Some(child.name), |
| uri: Some(child.uri), |
| }); |
| } |
| Ok(out_children) |
| } |
| |
| fn dictionary_from_map(in_obj: Map<String, serde_json::Value>) -> Result<fd::Dictionary, Error> { |
| let mut dict = fd::Dictionary { entries: vec![] }; |
| do_dictionary_from_map(in_obj, &mut dict)?; |
| Ok(dict) |
| } |
| |
| fn do_dictionary_from_map( |
| in_obj: Map<String, Value>, out: &mut fd::Dictionary, |
| ) -> Result<(), Error> { |
| for (k, v) in in_obj { |
| if let Some(value) = convert_value(v)? { |
| out.entries.push(fd::Entry { |
| key: k, |
| value: Some(value), |
| }); |
| } |
| } |
| Ok(()) |
| } |
| |
| fn convert_value(v: Value) -> Result<Option<Box<fd::Value>>, Error> { |
| Ok(match v { |
| Value::Null => None, |
| Value::Bool(b) => Some(Box::new(fd::Value::Bit(b))), |
| Value::Number(n) => { |
| if let Some(i) = n.as_i64() { |
| Some(Box::new(fd::Value::Inum(i))) |
| } else if let Some(f) = n.as_f64() { |
| Some(Box::new(fd::Value::Fnum(f))) |
| } else { |
| return Err(Error::Parse(format!("Number is out of range: {}", n))); |
| } |
| } |
| Value::String(s) => Some(Box::new(fd::Value::Str(s.clone()))), |
| Value::Array(a) => { |
| let mut values = vec![]; |
| for v in a { |
| if let Some(value) = convert_value(v)? { |
| values.push(Some(value)); |
| } |
| } |
| let vector = fd::Vector { values }; |
| Some(Box::new(fd::Value::Vec(vector))) |
| } |
| Value::Object(o) => { |
| let mut dict = fd::Dictionary { entries: vec![] }; |
| do_dictionary_from_map(o, &mut dict)?; |
| Some(Box::new(fd::Value::Dict(dict))) |
| } |
| }) |
| } |
| |
| fn capability_from_str(value: &str) -> Result<CapabilityType, Error> { |
| match value { |
| cm::SERVICE => Ok(CapabilityType::Service), |
| cm::DIRECTORY => Ok(CapabilityType::Directory), |
| _ => Err(Error::parse(format!("Unknown capability type: {}", value))), |
| } |
| } |
| |
| fn relation_from_str(value: &str) -> Result<Relation, Error> { |
| match value { |
| cm::REALM => Ok(Relation::Realm), |
| cm::SELF => Ok(Relation::Myself), |
| cm::CHILD => Ok(Relation::Child), |
| _ => Err(Error::parse(format!("Unknown relation: {}", value))), |
| } |
| } |
| |
| fn new_component_decl() -> ComponentDecl { |
| ComponentDecl { |
| program: None, |
| uses: None, |
| exposes: None, |
| offers: None, |
| facets: None, |
| children: None, |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use serde_json::json; |
| |
| fn translate_test(input: serde_json::value::Value, expected_output: ComponentDecl) { |
| let component_decl = translate(&format!("{}", input)).expect("translation failed"); |
| assert_eq!(component_decl, expected_output); |
| } |
| |
| macro_rules! test_translate { |
| ( |
| $( |
| $test_name:ident => { |
| input = $input:expr, |
| output = $result:expr, |
| }, |
| )+ |
| ) => { |
| $( |
| #[test] |
| fn $test_name() { |
| translate_test($input, $result); |
| } |
| )+ |
| } |
| } |
| |
| #[test] |
| fn test_translate_invalid_cm_fails() { |
| let input = json!({ |
| "exposes": [ |
| { |
| "type": "nothing", |
| "source_path": "/svc/fuchsia.logger.Log", |
| "source": { |
| "relation": "self" |
| }, |
| "target_path": "/svc/fuchsia.logger.Log" |
| } |
| ] |
| }); |
| |
| let expected_res: Result<ComponentDecl, Error> = Err(Error::parse( |
| "Pattern condition is not met at /exposes/0/type", |
| )); |
| let res = translate(&format!("{}", input)); |
| assert_eq!(format!("{:?}", res), format!("{:?}", expected_res)); |
| } |
| |
| #[test] |
| test_translate! { |
| test_translate_empty => { |
| input = json!({}), |
| output = new_component_decl(), |
| }, |
| test_translate_program => { |
| input = json!({ |
| "program": { |
| "binary": "bin/app" |
| } |
| }), |
| output = { |
| let program = fd::Dictionary{entries: vec![ |
| fd::Entry{ |
| key: "binary".to_string(), |
| value: Some(Box::new(fd::Value::Str("bin/app".to_string()))), |
| } |
| ]}; |
| let mut decl = new_component_decl(); |
| decl.program = Some(program); |
| decl |
| }, |
| }, |
| test_translate_dictionary_primitive => { |
| input = json!({ |
| "program": { |
| "string": "bar", |
| "int": -42, |
| "float": 3.14, |
| "bool": true, |
| "ignore": null |
| } |
| }), |
| output = { |
| let program = fd::Dictionary{entries: vec![ |
| fd::Entry{ |
| key: "bool".to_string(), |
| value: Some(Box::new(fd::Value::Bit(true))), |
| }, |
| fd::Entry{ |
| key: "float".to_string(), |
| value: Some(Box::new(fd::Value::Fnum(3.14))), |
| }, |
| fd::Entry{ |
| key: "int".to_string(), |
| value: Some(Box::new(fd::Value::Inum(-42))), |
| }, |
| fd::Entry{ |
| key: "string".to_string(), |
| value: Some(Box::new(fd::Value::Str("bar".to_string()))), |
| }, |
| ]}; |
| let mut decl = new_component_decl(); |
| decl.program = Some(program); |
| decl |
| }, |
| }, |
| test_translate_dictionary_nested => { |
| input = json!({ |
| "program": { |
| "obj": { |
| "array": [ |
| { |
| "string": "bar" |
| }, |
| -42 |
| ], |
| }, |
| "bool": true |
| } |
| }), |
| output = { |
| let dict_inner = fd::Dictionary{entries: vec![ |
| fd::Entry{ |
| key: "string".to_string(), |
| value: Some(Box::new(fd::Value::Str("bar".to_string()))), |
| }, |
| ]}; |
| let vector = fd::Vector{values: vec![ |
| Some(Box::new(fd::Value::Dict(dict_inner))), |
| Some(Box::new(fd::Value::Inum(-42))) |
| ]}; |
| let dict_outer = fd::Dictionary{entries: vec![ |
| fd::Entry{ |
| key: "array".to_string(), |
| value: Some(Box::new(fd::Value::Vec(vector))), |
| }, |
| ]}; |
| let program = fd::Dictionary{entries: vec![ |
| fd::Entry{ |
| key: "bool".to_string(), |
| value: Some(Box::new(fd::Value::Bit(true))), |
| }, |
| fd::Entry{ |
| key: "obj".to_string(), |
| value: Some(Box::new(fd::Value::Dict(dict_outer))), |
| }, |
| ]}; |
| let mut decl = new_component_decl(); |
| decl.program = Some(program); |
| decl |
| }, |
| }, |
| test_translate_uses => { |
| input = json!({ |
| "uses": [ |
| { |
| "type": "service", |
| "source_path": "/fonts/CoolFonts", |
| "target_path": "/svc/fuchsia.fonts.Provider" |
| }, |
| { |
| "type": "directory", |
| "source_path": "/data/assets", |
| "target_path": "/data/assets" |
| } |
| ] |
| }), |
| output = { |
| let uses = vec![ |
| UseDecl{ |
| type_: Some(CapabilityType::Service), |
| source_path: Some("/fonts/CoolFonts".to_string()), |
| target_path: Some("/svc/fuchsia.fonts.Provider".to_string()), |
| }, |
| UseDecl{ |
| type_: Some(CapabilityType::Directory), |
| source_path: Some("/data/assets".to_string()), |
| target_path: Some("/data/assets".to_string()), |
| }, |
| ]; |
| let mut decl = new_component_decl(); |
| decl.uses = Some(uses); |
| decl |
| }, |
| }, |
| test_translate_exposes => { |
| input = json!({ |
| "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" |
| } |
| ] |
| }), |
| output = { |
| let exposes = vec![ |
| ExposeDecl{ |
| type_: Some(CapabilityType::Service), |
| source_path: Some("/loggers/fuchsia.logger.Log".to_string()), |
| source: Some(RelativeId{ |
| relation: Some(Relation::Child), |
| child_name: Some("logger".to_string()), |
| }), |
| target_path: Some("/svc/fuchsia.logger.Log".to_string()), |
| }, |
| ExposeDecl{ |
| type_: Some(CapabilityType::Directory), |
| source_path: Some("/volumes/blobfs".to_string()), |
| source: Some(RelativeId{ |
| relation: Some(Relation::Myself), |
| child_name: None, |
| }), |
| target_path: Some("/volumes/blobfs".to_string()), |
| }, |
| ]; |
| let mut decl = new_component_decl(); |
| decl.exposes = Some(exposes); |
| decl |
| }, |
| }, |
| test_translate_offers => { |
| input = json!({ |
| "offers": [ |
| { |
| "type": "directory", |
| "source_path": "/data/assets", |
| "source": { |
| "relation": "realm" |
| }, |
| "targets": [ |
| { |
| "target_path": "/data/realm_assets", |
| "child_name": "echo2_server" |
| }, |
| { |
| "target_path": "/data/assets", |
| "child_name": "hello_world" |
| } |
| ] |
| }, |
| { |
| "type": "directory", |
| "source_path": "/data/config", |
| "source": { |
| "relation": "self" |
| }, |
| "targets": [ |
| { |
| "target_path": "/data/config", |
| "child_name": "netstack" |
| } |
| ] |
| }, |
| { |
| "type": "service", |
| "source_path": "/svc/fuchsia.logger.Log", |
| "source": { |
| "relation": "child", |
| "child_name": "logger" |
| }, |
| "targets": [ |
| { |
| "target_path": "/svc/fuchsia.logger.SysLog", |
| "child_name": "echo2_server" |
| } |
| ] |
| } |
| ] |
| }), |
| output = { |
| let offers = vec![ |
| OfferDecl{ |
| type_: Some(CapabilityType::Directory), |
| source_path: Some("/data/assets".to_string()), |
| source: Some(RelativeId{ |
| relation: Some(Relation::Realm), |
| child_name: None |
| }), |
| targets: Some(vec![ |
| OfferTarget{ |
| target_path: Some("/data/realm_assets".to_string()), |
| child_name: Some("echo2_server".to_string()), |
| }, |
| OfferTarget{ |
| target_path: Some("/data/assets".to_string()), |
| child_name: Some("hello_world".to_string()), |
| }, |
| ]), |
| }, |
| OfferDecl{ |
| type_: Some(CapabilityType::Directory), |
| source_path: Some("/data/config".to_string()), |
| source: Some(RelativeId{ |
| relation: Some(Relation::Myself), |
| child_name: None |
| }), |
| targets: Some(vec![ |
| OfferTarget{ |
| target_path: Some("/data/config".to_string()), |
| child_name: Some("netstack".to_string()), |
| }, |
| ]), |
| }, |
| OfferDecl{ |
| type_: Some(CapabilityType::Service), |
| source_path: Some("/svc/fuchsia.logger.Log".to_string()), |
| source: Some(RelativeId{ |
| relation: Some(Relation::Child), |
| child_name: Some("logger".to_string()), |
| }), |
| targets: Some(vec![ |
| OfferTarget{ |
| target_path: Some("/svc/fuchsia.logger.SysLog".to_string()), |
| child_name: Some("echo2_server".to_string()), |
| }, |
| ]), |
| }, |
| ]; |
| let mut decl = new_component_decl(); |
| decl.offers = Some(offers); |
| decl |
| }, |
| }, |
| test_translate_children => { |
| input = json!({ |
| "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" |
| } |
| ] |
| }), |
| output = { |
| let children = vec![ |
| ChildDecl{ |
| name: Some("logger".to_string()), |
| uri: Some("fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm".to_string()), |
| }, |
| ChildDecl{ |
| name: Some("echo2_server".to_string()), |
| uri: Some("fuchsia-pkg://fuchsia.com/echo2_server/stable#meta/echo2_server.cm".to_string()), |
| }, |
| ]; |
| let mut decl = new_component_decl(); |
| decl.children = Some(children); |
| decl |
| }, |
| }, |
| test_translate_facets => { |
| input = json!({ |
| "facets": { |
| "authors": [ |
| "me", |
| "you" |
| ], |
| "title": "foo", |
| "year": 2018 |
| } |
| }), |
| output = { |
| let vector = fd::Vector{values: vec![ |
| Some(Box::new(fd::Value::Str("me".to_string()))), |
| Some(Box::new(fd::Value::Str("you".to_string()))), |
| ]}; |
| let facets = fd::Dictionary{entries: vec![ |
| fd::Entry{ |
| key: "authors".to_string(), |
| value: Some(Box::new(fd::Value::Vec(vector))), |
| }, |
| fd::Entry{ |
| key: "title".to_string(), |
| value: Some(Box::new(fd::Value::Str("foo".to_string()))), |
| }, |
| fd::Entry{ |
| key: "year".to_string(), |
| value: Some(Box::new(fd::Value::Inum(2018))), |
| }, |
| ]}; |
| let mut decl = new_component_decl(); |
| decl.facets = Some(facets); |
| decl |
| }, |
| }, |
| test_translate_all_sections => { |
| input = json!({ |
| "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 |
| } |
| }), |
| output = { |
| let program = fd::Dictionary{entries: vec![ |
| fd::Entry{ |
| key: "binary".to_string(), |
| value: Some(Box::new(fd::Value::Str("bin/app".to_string()))), |
| }, |
| ]}; |
| let uses = vec![ |
| UseDecl{ |
| type_: Some(CapabilityType::Service), |
| source_path: Some("/fonts/CoolFonts".to_string()), |
| target_path: Some("/svc/fuchsia.fonts.Provider".to_string()), |
| }, |
| ]; |
| let exposes = vec![ |
| ExposeDecl{ |
| type_: Some(CapabilityType::Directory), |
| source_path: Some("/volumes/blobfs".to_string()), |
| source: Some(RelativeId{ |
| relation: Some(Relation::Myself), |
| child_name: None, |
| }), |
| target_path: Some("/volumes/blobfs".to_string()), |
| }, |
| ]; |
| let offers = vec![ |
| OfferDecl{ |
| type_: Some(CapabilityType::Service), |
| source_path: Some("/svc/fuchsia.logger.Log".to_string()), |
| source: Some(RelativeId{ |
| relation: Some(Relation::Child), |
| child_name: Some("logger".to_string()), |
| }), |
| targets: Some(vec![ |
| OfferTarget{ |
| target_path: Some("/svc/fuchsia.logger.Log".to_string()), |
| child_name: Some("netstack".to_string()), |
| }, |
| ]), |
| }, |
| ]; |
| let children = vec![ |
| ChildDecl{ |
| name: Some("logger".to_string()), |
| uri: Some("fuchsia-pkg://fuchsia.com/logger/stable#meta/logger.cm".to_string()), |
| }, |
| ChildDecl{ |
| name: Some("netstack".to_string()), |
| uri: Some("fuchsia-pkg://fuchsia.com/netstack/stable#meta/netstack.cm".to_string()), |
| }, |
| ]; |
| let facets = fd::Dictionary{entries: vec![ |
| fd::Entry{ |
| key: "author".to_string(), |
| value: Some(Box::new(fd::Value::Str("Fuchsia".to_string()))), |
| }, |
| fd::Entry{ |
| key: "year".to_string(), |
| value: Some(Box::new(fd::Value::Inum(2018))), |
| }, |
| ]}; |
| ComponentDecl{ |
| program: Some(program), |
| uses: Some(uses), |
| exposes: Some(exposes), |
| offers: Some(offers), |
| children: Some(children), |
| facets: Some(facets) |
| } |
| }, |
| }, |
| } |
| } |