| // Copyright 2020 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 { |
| argh::FromArgs, |
| cm_types::{symmetrical_enums, Url}, |
| cml::error::{Error, Location}, |
| fidl::encoding::encode_persistent, |
| fidl_fuchsia_component_internal as component_internal, fidl_fuchsia_sys2 as fsys, |
| serde::Deserialize, |
| serde_json5, |
| std::{ |
| collections::HashSet, |
| convert::{TryFrom, TryInto}, |
| fs::{self, File}, |
| io::Write, |
| path::PathBuf, |
| }, |
| }; |
| |
| #[derive(Deserialize, Debug, Default)] |
| #[serde(deny_unknown_fields)] |
| struct Config { |
| debug: Option<bool>, |
| list_children_batch_size: Option<u32>, |
| security_policy: Option<SecurityPolicy>, |
| namespace_capabilities: Option<Vec<cml::Capability>>, |
| use_builtin_process_launcher: Option<bool>, |
| maintain_utc_clock: Option<bool>, |
| num_threads: Option<u32>, |
| builtin_pkg_resolver: Option<BuiltinPkgResolver>, |
| out_dir_contents: Option<OutDirContents>, |
| root_component_url: Option<Url>, |
| component_id_index_path: Option<String>, |
| log_all_events: Option<bool>, |
| } |
| |
| #[derive(Deserialize, Debug)] |
| #[serde(rename_all = "snake_case")] |
| enum BuiltinPkgResolver { |
| None, |
| AppmgrBridge, |
| } |
| |
| symmetrical_enums!(BuiltinPkgResolver, component_internal::BuiltinPkgResolver, None, AppmgrBridge); |
| |
| impl std::default::Default for BuiltinPkgResolver { |
| fn default() -> Self { |
| BuiltinPkgResolver::None |
| } |
| } |
| |
| #[derive(Deserialize, Debug, Default)] |
| #[serde(deny_unknown_fields)] |
| pub struct SecurityPolicy { |
| job_policy: Option<JobPolicyAllowlists>, |
| capability_policy: Option<Vec<CapabilityAllowlistEntry>>, |
| debug_registration_policy: Option<Vec<DebugRegistrationAllowlistEntry>>, |
| } |
| |
| #[derive(Deserialize, Debug, Default)] |
| #[serde(deny_unknown_fields)] |
| pub struct JobPolicyAllowlists { |
| ambient_mark_vmo_exec: Option<Vec<String>>, |
| main_process_critical: Option<Vec<String>>, |
| create_raw_processes: Option<Vec<String>>, |
| } |
| |
| #[derive(Deserialize, Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] |
| #[repr(u32)] |
| #[serde(rename_all = "snake_case")] |
| pub enum OutDirContents { |
| None, |
| Hub, |
| Svc, |
| } |
| |
| symmetrical_enums!(OutDirContents, component_internal::OutDirContents, None, Hub, Svc); |
| |
| impl std::default::Default for OutDirContents { |
| fn default() -> Self { |
| OutDirContents::Hub |
| } |
| } |
| |
| #[derive(Deserialize, Debug, Clone)] |
| #[serde(rename_all = "lowercase")] |
| pub enum CapabilityTypeName { |
| Directory, |
| Event, |
| Protocol, |
| Service, |
| Storage, |
| Runner, |
| Resolver, |
| } |
| |
| impl Into<component_internal::AllowlistedCapability> for CapabilityTypeName { |
| fn into(self) -> component_internal::AllowlistedCapability { |
| match &self { |
| CapabilityTypeName::Directory => component_internal::AllowlistedCapability::Directory( |
| component_internal::AllowlistedDirectory::EMPTY, |
| ), |
| CapabilityTypeName::Event => component_internal::AllowlistedCapability::Event( |
| component_internal::AllowlistedEvent::EMPTY, |
| ), |
| CapabilityTypeName::Protocol => component_internal::AllowlistedCapability::Protocol( |
| component_internal::AllowlistedProtocol::EMPTY, |
| ), |
| CapabilityTypeName::Service => component_internal::AllowlistedCapability::Service( |
| component_internal::AllowlistedService::EMPTY, |
| ), |
| CapabilityTypeName::Storage => component_internal::AllowlistedCapability::Storage( |
| component_internal::AllowlistedStorage::EMPTY, |
| ), |
| CapabilityTypeName::Runner => component_internal::AllowlistedCapability::Runner( |
| component_internal::AllowlistedRunner::EMPTY, |
| ), |
| CapabilityTypeName::Resolver => component_internal::AllowlistedCapability::Resolver( |
| component_internal::AllowlistedResolver::EMPTY, |
| ), |
| } |
| } |
| } |
| |
| #[derive(Deserialize, Debug, Clone)] |
| #[serde(rename_all = "lowercase")] |
| pub enum DebugRegistrationTypeName { |
| Protocol, |
| } |
| |
| impl Into<component_internal::AllowlistedDebugRegistration> for DebugRegistrationTypeName { |
| fn into(self) -> component_internal::AllowlistedDebugRegistration { |
| match &self { |
| DebugRegistrationTypeName::Protocol => { |
| component_internal::AllowlistedDebugRegistration::Protocol( |
| component_internal::AllowlistedProtocol::EMPTY, |
| ) |
| } |
| } |
| } |
| } |
| |
| #[derive(Deserialize, Debug, Clone)] |
| #[serde(rename_all = "lowercase")] |
| pub enum CapabilityFrom { |
| Capability, |
| Component, |
| Framework, |
| } |
| |
| impl Into<fsys::Ref> for CapabilityFrom { |
| fn into(self) -> fsys::Ref { |
| match &self { |
| CapabilityFrom::Capability => { |
| fsys::Ref::Capability(fsys::CapabilityRef { name: "".into() }) |
| } |
| CapabilityFrom::Component => fsys::Ref::Self_(fsys::SelfRef {}), |
| CapabilityFrom::Framework => fsys::Ref::Framework(fsys::FrameworkRef {}), |
| } |
| } |
| } |
| |
| #[derive(Deserialize, Debug, Default)] |
| #[serde(deny_unknown_fields)] |
| pub struct CapabilityAllowlistEntry { |
| source_moniker: Option<String>, |
| source_name: Option<String>, |
| source: Option<CapabilityFrom>, |
| capability: Option<CapabilityTypeName>, |
| target_monikers: Option<Vec<String>>, |
| } |
| |
| #[derive(Deserialize, Debug, Default)] |
| #[serde(deny_unknown_fields)] |
| pub struct DebugRegistrationAllowlistEntry { |
| source_moniker: Option<String>, |
| source_name: Option<String>, |
| debug: Option<DebugRegistrationTypeName>, |
| target_moniker: Option<String>, |
| environment_name: Option<String>, |
| } |
| |
| impl TryFrom<Config> for component_internal::Config { |
| type Error = Error; |
| |
| fn try_from(config: Config) -> Result<Self, Error> { |
| // Validate "namespace_capabilities". |
| if let Some(capabilities) = config.namespace_capabilities.as_ref() { |
| let mut used_ids = HashSet::new(); |
| for capability in capabilities { |
| Config::validate_capability(capability, &mut used_ids)?; |
| } |
| } |
| |
| Ok(Self { |
| debug: config.debug, |
| use_builtin_process_launcher: config.use_builtin_process_launcher, |
| maintain_utc_clock: config.maintain_utc_clock, |
| list_children_batch_size: config.list_children_batch_size, |
| security_policy: Some(translate_security_policy(config.security_policy)), |
| builtin_pkg_resolver: match config.builtin_pkg_resolver { |
| Some(builtin_pkg_resolver) => Some(builtin_pkg_resolver.into()), |
| None => None, |
| }, |
| namespace_capabilities: config |
| .namespace_capabilities |
| .as_ref() |
| .map(|c| cml::translate::translate_capabilities(c)) |
| .transpose()?, |
| num_threads: config.num_threads, |
| out_dir_contents: match config.out_dir_contents { |
| Some(out_dir_contents) => Some(out_dir_contents.into()), |
| None => None, |
| }, |
| root_component_url: match config.root_component_url { |
| Some(root_component_url) => Some(root_component_url.as_str().to_string()), |
| None => None, |
| }, |
| component_id_index_path: config.component_id_index_path, |
| log_all_events: config.log_all_events, |
| ..Self::EMPTY |
| }) |
| } |
| } |
| |
| // TODO: Instead of returning a "default" security_policy when it's not specified in JSON, |
| // could we return None instead? |
| fn translate_security_policy( |
| security_policy: Option<SecurityPolicy>, |
| ) -> component_internal::SecurityPolicy { |
| let SecurityPolicy { job_policy, capability_policy, debug_registration_policy } = |
| security_policy.unwrap_or_default(); |
| component_internal::SecurityPolicy { |
| job_policy: job_policy.map(translate_job_policy), |
| capability_policy: capability_policy.map(translate_capability_policy), |
| debug_registration_policy: debug_registration_policy |
| .map(translate_debug_registration_policy), |
| ..component_internal::SecurityPolicy::EMPTY |
| } |
| } |
| |
| fn translate_job_policy( |
| job_policy: JobPolicyAllowlists, |
| ) -> component_internal::JobPolicyAllowlists { |
| component_internal::JobPolicyAllowlists { |
| ambient_mark_vmo_exec: job_policy.ambient_mark_vmo_exec, |
| main_process_critical: job_policy.main_process_critical, |
| create_raw_processes: job_policy.create_raw_processes, |
| ..component_internal::JobPolicyAllowlists::EMPTY |
| } |
| } |
| |
| fn translate_capability_policy( |
| capability_policy: Vec<CapabilityAllowlistEntry>, |
| ) -> component_internal::CapabilityPolicyAllowlists { |
| let allowlist = capability_policy |
| .iter() |
| .map(|e| component_internal::CapabilityAllowlistEntry { |
| source_moniker: e.source_moniker.clone(), |
| source_name: e.source_name.clone(), |
| source: e.source.clone().map_or_else(|| None, |t| Some(t.into())), |
| capability: e.capability.clone().map_or_else(|| None, |t| Some(t.into())), |
| target_monikers: e.target_monikers.clone(), |
| ..component_internal::CapabilityAllowlistEntry::EMPTY |
| }) |
| .collect::<Vec<_>>(); |
| component_internal::CapabilityPolicyAllowlists { |
| allowlist: Some(allowlist), |
| ..component_internal::CapabilityPolicyAllowlists::EMPTY |
| } |
| } |
| |
| fn translate_debug_registration_policy( |
| debug_registration_policy: Vec<DebugRegistrationAllowlistEntry>, |
| ) -> component_internal::DebugRegistrationPolicyAllowlists { |
| let allowlist = debug_registration_policy |
| .iter() |
| .map(|e| component_internal::DebugRegistrationAllowlistEntry { |
| source_moniker: e.source_moniker.clone(), |
| source_name: e.source_name.clone(), |
| debug: e.debug.clone().map_or_else(|| None, |t| Some(t.into())), |
| target_moniker: e.target_moniker.clone(), |
| environment_name: e.environment_name.clone(), |
| ..component_internal::DebugRegistrationAllowlistEntry::EMPTY |
| }) |
| .collect::<Vec<_>>(); |
| component_internal::DebugRegistrationPolicyAllowlists { |
| allowlist: Some(allowlist), |
| ..component_internal::DebugRegistrationPolicyAllowlists::EMPTY |
| } |
| } |
| |
| macro_rules! extend_if_unset { |
| ( $($target:expr, $value:expr, $field:ident)+ ) => { |
| $( |
| $target.$field = match (&$target.$field, &$value.$field) { |
| (Some(_), Some(_)) => { |
| return Err(Error::parse( |
| format!("Conflicting field found: {:?}", stringify!($field)), |
| None, |
| None, |
| )) |
| } |
| (None, Some(_)) => $value.$field, |
| (Some(_), None) => $target.$field, |
| (&None, &None) => None, |
| }; |
| )+ |
| }; |
| } |
| |
| impl Config { |
| fn from_json_file(path: &PathBuf) -> Result<Self, Error> { |
| let data = fs::read_to_string(path)?; |
| serde_json5::from_str(&data).map_err(|e| { |
| let serde_json5::Error::Message { location, msg } = e; |
| let location = location.map(|l| Location { line: l.line, column: l.column }); |
| Error::parse(msg, location, Some(path)) |
| }) |
| } |
| |
| fn extend(mut self, another: Config) -> Result<Self, Error> { |
| extend_if_unset!(self, another, debug); |
| extend_if_unset!(self, another, use_builtin_process_launcher); |
| extend_if_unset!(self, another, maintain_utc_clock); |
| extend_if_unset!(self, another, list_children_batch_size); |
| extend_if_unset!(self, another, security_policy); |
| extend_if_unset!(self, another, namespace_capabilities); |
| extend_if_unset!(self, another, num_threads); |
| extend_if_unset!(self, another, builtin_pkg_resolver); |
| extend_if_unset!(self, another, out_dir_contents); |
| extend_if_unset!(self, another, root_component_url); |
| extend_if_unset!(self, another, component_id_index_path); |
| extend_if_unset!(self, another, log_all_events); |
| Ok(self) |
| } |
| |
| fn validate_capability( |
| capability: &cml::Capability, |
| used_ids: &mut HashSet<String>, |
| ) -> Result<(), Error> { |
| if capability.directory.is_some() && capability.path.is_none() { |
| return Err(Error::validate("\"path\" should be present with \"directory\"")); |
| } |
| if capability.directory.is_some() && capability.rights.is_none() { |
| return Err(Error::validate("\"rights\" should be present with \"directory\"")); |
| } |
| if capability.storage.is_some() { |
| return Err(Error::validate("\"storage\" is not supported for namespace capabilities")); |
| } |
| if capability.runner.is_some() { |
| return Err(Error::validate("\"runner\" is not supported for namespace capabilities")); |
| } |
| if capability.resolver.is_some() { |
| return Err(Error::validate( |
| "\"resolver\" is not supported for namespace capabilities", |
| )); |
| } |
| |
| // Disallow multiple capability ids of the same name. |
| let capability_ids = cml::CapabilityId::from_capability(capability)?; |
| for capability_id in capability_ids { |
| if !used_ids.insert(capability_id.to_string()) { |
| return Err(Error::validate(format!( |
| "\"{}\" is a duplicate \"capability\" name", |
| capability_id, |
| ))); |
| } |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| #[derive(Debug, Default, FromArgs)] |
| /// Create a binary config and populate it with data from .json file. |
| struct Args { |
| /// path to a JSON configuration file |
| #[argh(option)] |
| input: Vec<PathBuf>, |
| |
| /// path to the output binary config file |
| #[argh(option)] |
| output: PathBuf, |
| } |
| |
| pub fn from_args() -> Result<(), Error> { |
| compile(argh::from_env()) |
| } |
| |
| fn compile(args: Args) -> Result<(), Error> { |
| let configs = |
| args.input.iter().map(Config::from_json_file).collect::<Result<Vec<Config>, _>>()?; |
| let config_json = |
| configs.into_iter().try_fold(Config::default(), |acc, next| acc.extend(next))?; |
| |
| let mut config_fidl: component_internal::Config = config_json.try_into()?; |
| let bytes = encode_persistent(&mut config_fidl).map_err(|e| Error::FidlEncoding(e))?; |
| let mut file = File::create(args.output).map_err(|e| Error::Io(e))?; |
| file.write_all(&bytes).map_err(|e| Error::Io(e))?; |
| Ok(()) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use { |
| fidl::encoding::decode_persistent, fidl_fuchsia_io2 as fio2, fidl_fuchsia_sys2 as fsys, |
| matches::assert_matches, std::io::Read, tempfile::TempDir, |
| }; |
| |
| fn compile_str(input: &str) -> Result<component_internal::Config, Error> { |
| let tmp_dir = TempDir::new().unwrap(); |
| let input_path = tmp_dir.path().join("config.json"); |
| let output_path = tmp_dir.path().join("config.fidl"); |
| File::create(&input_path).unwrap().write_all(input.as_bytes()).unwrap(); |
| let args = Args { output: output_path.clone(), input: vec![input_path] }; |
| compile(args)?; |
| let mut bytes = Vec::new(); |
| File::open(output_path)?.read_to_end(&mut bytes)?; |
| let config: component_internal::Config = decode_persistent(&bytes)?; |
| Ok(config) |
| } |
| |
| #[test] |
| fn test_compile() { |
| let input = r#"{ |
| debug: true, |
| list_children_batch_size: 123, |
| maintain_utc_clock: false, |
| use_builtin_process_launcher: true, |
| builtin_pkg_resolver: "appmgr_bridge", |
| security_policy: { |
| job_policy: { |
| main_process_critical: [ "/", "/bar" ], |
| ambient_mark_vmo_exec: ["/foo"], |
| create_raw_processes: ["/baz"], |
| }, |
| capability_policy: [ |
| { |
| source_moniker: "<component_manager>", |
| source: "component", |
| source_name: "fuchsia.kernel.RootResource", |
| capability: "protocol", |
| target_monikers: ["/root", "/root/bootstrap", "/root/core"], |
| }, |
| { |
| source_moniker: "/foo/bar", |
| source_name: "running", |
| source: "framework", |
| capability: "event", |
| target_monikers: ["/foo/bar", "/foo/bar/baz"], |
| }, |
| ], |
| debug_registration_policy: [ |
| { |
| source_moniker: "/foo/bar", |
| source_name: "fuchsia.kernel.RootResource", |
| debug: "protocol", |
| target_moniker: "/foo", |
| environment_name: "my_env", |
| }, |
| ] |
| }, |
| namespace_capabilities: [ |
| { |
| protocol: "foo_svc", |
| }, |
| { |
| directory: "bar_dir", |
| path: "/bar", |
| rights: [ "connect" ], |
| }, |
| ], |
| num_threads: 321, |
| out_dir_contents: "svc", |
| root_component_url: "fuchsia-pkg://fuchsia.com/foo#meta/foo.cmx", |
| component_id_index_path: "/this/is/an/absolute/path", |
| log_all_events: true, |
| }"#; |
| let config = compile_str(input).expect("failed to compile"); |
| assert_eq!( |
| config, |
| component_internal::Config { |
| debug: Some(true), |
| maintain_utc_clock: Some(false), |
| use_builtin_process_launcher: Some(true), |
| list_children_batch_size: Some(123), |
| builtin_pkg_resolver: Some(component_internal::BuiltinPkgResolver::AppmgrBridge), |
| security_policy: Some(component_internal::SecurityPolicy { |
| job_policy: Some(component_internal::JobPolicyAllowlists { |
| main_process_critical: Some(vec!["/".to_string(), "/bar".to_string()]), |
| ambient_mark_vmo_exec: Some(vec!["/foo".to_string()]), |
| create_raw_processes: Some(vec!["/baz".to_string()]), |
| ..component_internal::JobPolicyAllowlists::EMPTY |
| }), |
| capability_policy: Some(component_internal::CapabilityPolicyAllowlists { |
| allowlist: Some(vec![ |
| component_internal::CapabilityAllowlistEntry { |
| source_moniker: Some("<component_manager>".to_string()), |
| source_name: Some("fuchsia.kernel.RootResource".to_string()), |
| source: Some(fsys::Ref::Self_(fsys::SelfRef {})), |
| capability: Some( |
| component_internal::AllowlistedCapability::Protocol( |
| component_internal::AllowlistedProtocol::EMPTY |
| ) |
| ), |
| target_monikers: Some(vec![ |
| "/root".to_string(), |
| "/root/bootstrap".to_string(), |
| "/root/core".to_string() |
| ]), |
| ..component_internal::CapabilityAllowlistEntry::EMPTY |
| }, |
| component_internal::CapabilityAllowlistEntry { |
| source_moniker: Some("/foo/bar".to_string()), |
| capability: Some(component_internal::AllowlistedCapability::Event( |
| component_internal::AllowlistedEvent::EMPTY |
| )), |
| source_name: Some("running".to_string()), |
| source: Some(fsys::Ref::Framework(fsys::FrameworkRef {})), |
| target_monikers: Some(vec![ |
| "/foo/bar".to_string(), |
| "/foo/bar/baz".to_string() |
| ]), |
| ..component_internal::CapabilityAllowlistEntry::EMPTY |
| }, |
| ]), |
| ..component_internal::CapabilityPolicyAllowlists::EMPTY |
| }), |
| debug_registration_policy: Some( |
| component_internal::DebugRegistrationPolicyAllowlists { |
| allowlist: Some(vec![ |
| component_internal::DebugRegistrationAllowlistEntry { |
| source_moniker: Some("/foo/bar".to_string()), |
| source_name: Some("fuchsia.kernel.RootResource".to_string()), |
| debug: Some( |
| component_internal::AllowlistedDebugRegistration::Protocol( |
| component_internal::AllowlistedProtocol::EMPTY |
| ) |
| ), |
| target_moniker: Some("/foo".to_string()), |
| environment_name: Some("my_env".to_string()), |
| ..component_internal::DebugRegistrationAllowlistEntry::EMPTY |
| } |
| ]), |
| ..component_internal::DebugRegistrationPolicyAllowlists::EMPTY |
| } |
| ), |
| ..component_internal::SecurityPolicy::EMPTY |
| }), |
| namespace_capabilities: Some(vec![ |
| fsys::CapabilityDecl::Protocol(fsys::ProtocolDecl { |
| name: Some("foo_svc".into()), |
| source_path: Some("/svc/foo_svc".into()), |
| ..fsys::ProtocolDecl::EMPTY |
| }), |
| fsys::CapabilityDecl::Directory(fsys::DirectoryDecl { |
| name: Some("bar_dir".into()), |
| source_path: Some("/bar".into()), |
| rights: Some(fio2::Operations::Connect), |
| ..fsys::DirectoryDecl::EMPTY |
| }), |
| ]), |
| num_threads: Some(321), |
| out_dir_contents: Some(component_internal::OutDirContents::Svc), |
| root_component_url: Some("fuchsia-pkg://fuchsia.com/foo#meta/foo.cmx".to_string()), |
| component_id_index_path: Some("/this/is/an/absolute/path".to_string()), |
| log_all_events: Some(true), |
| ..component_internal::Config::EMPTY |
| } |
| ); |
| } |
| |
| #[test] |
| fn test_validate_namespace_capabilities() { |
| { |
| let input = r#"{ |
| namespace_capabilities: [ |
| { |
| protocol: "foo", |
| }, |
| { |
| directory: "foo", |
| path: "/foo", |
| rights: [ "connect" ], |
| }, |
| ], |
| }"#; |
| assert_matches!( |
| compile_str(input), |
| Err(Error::Validate { schema_name: None, err, .. } ) |
| if &err == "\"foo\" is a duplicate \"capability\" name" |
| ); |
| } |
| { |
| let input = r#"{ |
| namespace_capabilities: [ |
| { |
| directory: "foo", |
| path: "/foo", |
| }, |
| ], |
| }"#; |
| assert_matches!( |
| compile_str(input), |
| Err(Error::Validate { schema_name: None, err, .. } ) |
| if &err == "\"rights\" should be present with \"directory\"" |
| ); |
| } |
| { |
| let input = r#"{ |
| namespace_capabilities: [ |
| { |
| directory: "foo", |
| rights: [ "connect" ], |
| }, |
| ], |
| }"#; |
| assert_matches!( |
| compile_str(input), |
| Err(Error::Validate { schema_name: None, err, .. } ) |
| if &err == "\"path\" should be present with \"directory\"" |
| ); |
| } |
| } |
| |
| #[test] |
| fn test_compile_conflict() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let output_path = tmp_dir.path().join("config"); |
| |
| let input_path = tmp_dir.path().join("foo.json"); |
| let input = "{\"debug\": true,}"; |
| File::create(&input_path).unwrap().write_all(input.as_bytes()).unwrap(); |
| |
| let another_input_path = tmp_dir.path().join("bar.json"); |
| let another_input = "{\"debug\": false,}"; |
| File::create(&another_input_path).unwrap().write_all(another_input.as_bytes()).unwrap(); |
| |
| let args = |
| Args { output: output_path.clone(), input: vec![input_path, another_input_path] }; |
| assert_matches!(compile(args), Err(Error::Parse { .. })); |
| } |
| |
| #[test] |
| fn test_merge() -> Result<(), Error> { |
| let tmp_dir = TempDir::new().unwrap(); |
| let output_path = tmp_dir.path().join("config"); |
| |
| let input_path = tmp_dir.path().join("foo.json"); |
| let input = "{\"debug\": true,}"; |
| File::create(&input_path).unwrap().write_all(input.as_bytes()).unwrap(); |
| |
| let another_input_path = tmp_dir.path().join("bar.json"); |
| let another_input = "{\"list_children_batch_size\": 42,}"; |
| File::create(&another_input_path).unwrap().write_all(another_input.as_bytes()).unwrap(); |
| |
| let args = |
| Args { output: output_path.clone(), input: vec![input_path, another_input_path] }; |
| compile(args)?; |
| |
| let mut bytes = Vec::new(); |
| File::open(output_path)?.read_to_end(&mut bytes)?; |
| let config: component_internal::Config = decode_persistent(&bytes)?; |
| assert_eq!(config.debug, Some(true)); |
| assert_eq!(config.list_children_batch_size, Some(42)); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_invalid_component_url() { |
| let input = r#"{ |
| root_component_url: "not quite a valid Url", |
| }"#; |
| assert_matches!(compile_str(input), Err(Error::Parse { .. })); |
| } |
| } |