blob: 42ad7fe9a3eec463e009eb103818ae3dca26f2bb [file] [log] [blame]
// 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},
translate::CompileOptions,
},
fidl::persist,
fidl_fuchsia_component_decl as fdecl, fidl_fuchsia_component_internal as component_internal,
itertools::Itertools,
serde::Deserialize,
std::{
collections::HashSet,
fs::{self, File},
io::Write,
path::PathBuf,
},
};
fn remove_duplicates<T: Ord>(mut v: Vec<T>) -> Vec<T> {
v.sort();
v.dedup();
v
}
fn merge_option<T, F>(left: Option<T>, right: Option<T>, merge_fn: F) -> Result<Option<T>, Error>
where
F: FnOnce(T, T) -> Result<T, Error>,
{
match (left, right) {
(left @ Some(_), None) => Ok(left),
(None, right @ Some(_)) => Ok(right),
(Some(l), Some(r)) => Ok(Some(merge_fn(l, r)?)),
(None, None) => Ok(None),
}
}
macro_rules! deep_merge_field {
($target:ident, $other:expr, $field:ident) => {
merge_option($target.$field, $other.$field, |l, r| l.merge(r))?
};
}
macro_rules! merge_field {
($target:ident, $other:expr, $field:ident) => {
merge_option($target.$field, $other.$field, |_, _| {
Err(Error::parse(
format!("Conflicting field found: {:?}", stringify!($field)),
None,
None,
))
})?
};
}
macro_rules! merge_vec {
($target:ident, $other:expr, $field:ident) => {
merge_option($target.$field, $other.$field, |mut l, mut r| {
l.append(&mut r);
Ok(remove_duplicates(l))
})?
};
}
const SESSION_MONIKER: &str = "/core/session-manager/session:session";
/// We use types for the platform capabilities so that the product owners get a nice error
/// indicating their options if they choose an invalid capability.
#[derive(Deserialize, Debug, Eq, PartialEq, Hash, Clone)]
enum PlatformCapability {
#[serde(rename = "fuchsia.lowpan.device.DeviceExtraConnector")]
Lowpan,
#[serde(rename = "fuchsia.kernel.VmexResource")]
VmexResource,
}
impl PlatformCapability {
/// Convert the capability back to a string.
fn source_name(&self) -> &str {
match self {
&PlatformCapability::Lowpan => "fuchsia.lowpan.device.DeviceExtraConnector",
&PlatformCapability::VmexResource => "fuchsia.kernel.VmexResource",
}
}
/// Map the capability to the moniker of the source.
fn source_moniker(&self) -> &str {
match self {
&PlatformCapability::Lowpan => "/core/lowpanservice",
&PlatformCapability::VmexResource => "<component_manager>",
}
}
}
#[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>>,
builtin_capabilities: Option<Vec<cml::Capability>>,
use_builtin_process_launcher: Option<bool>,
maintain_utc_clock: Option<bool>,
num_threads: Option<u32>,
root_component_url: Option<Url>,
component_id_index_path: Option<String>,
log_destination: Option<LogDestination>,
log_all_events: Option<bool>,
builtin_boot_resolver: Option<BuiltinBootResolver>,
realm_builder_resolver_and_runner: Option<RealmBuilderResolverAndRunner>,
enable_introspection: Option<bool>,
abi_revision_policy: Option<AbiRevisionPolicy>,
vmex_source: Option<VmexSource>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
enum LogDestination {
Syslog,
Klog,
}
symmetrical_enums!(LogDestination, component_internal::LogDestination, Syslog, Klog);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
enum BuiltinBootResolver {
None,
Boot,
}
symmetrical_enums!(BuiltinBootResolver, component_internal::BuiltinBootResolver, None, Boot);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
enum RealmBuilderResolverAndRunner {
None,
Namespace,
}
symmetrical_enums!(
RealmBuilderResolverAndRunner,
component_internal::RealmBuilderResolverAndRunner,
None,
Namespace
);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum AbiRevisionPolicy {
AllowAll,
EnforcePresenceOnly,
EnforcePresenceAndCompatibility,
}
symmetrical_enums!(
AbiRevisionPolicy,
component_internal::AbiRevisionPolicy,
AllowAll,
EnforcePresenceOnly,
EnforcePresenceAndCompatibility
);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum VmexSource {
SystemResource,
Namespace,
}
symmetrical_enums!(VmexSource, component_internal::VmexSource, SystemResource, Namespace);
#[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>>,
child_policy: Option<ChildPolicyAllowlists>,
}
impl SecurityPolicy {
fn merge(self, another: SecurityPolicy) -> Result<Self, Error> {
return Ok(SecurityPolicy {
job_policy: deep_merge_field!(self, another, job_policy),
capability_policy: merge_option(
self.capability_policy,
another.capability_policy,
CapabilityAllowlistEntry::merge_vecs,
)?,
debug_registration_policy: merge_field!(self, another, debug_registration_policy),
child_policy: deep_merge_field!(self, another, child_policy),
});
}
}
#[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>>,
}
impl JobPolicyAllowlists {
fn merge(self, another: JobPolicyAllowlists) -> Result<Self, Error> {
return Ok(JobPolicyAllowlists {
ambient_mark_vmo_exec: merge_vec!(self, another, ambient_mark_vmo_exec),
main_process_critical: merge_vec!(self, another, main_process_critical),
create_raw_processes: merge_vec!(self, another, create_raw_processes),
});
}
}
#[derive(Deserialize, Debug, Default)]
#[serde(deny_unknown_fields)]
pub struct ChildPolicyAllowlists {
reboot_on_terminate: Option<Vec<String>>,
}
impl ChildPolicyAllowlists {
fn merge(self, another: ChildPolicyAllowlists) -> Result<Self, Error> {
return Ok(ChildPolicyAllowlists {
reboot_on_terminate: merge_vec!(self, another, reboot_on_terminate),
});
}
}
#[derive(Deserialize, Debug, Clone, PartialEq, Hash, Eq, Ord, PartialOrd)]
#[serde(rename_all = "lowercase")]
pub enum CapabilityTypeName {
Directory,
Protocol,
Service,
Storage,
Runner,
Resolver,
}
impl std::fmt::Display for CapabilityTypeName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let display_name = match &self {
CapabilityTypeName::Directory => "directory",
CapabilityTypeName::Protocol => "protocol",
CapabilityTypeName::Service => "service",
CapabilityTypeName::Storage => "storage",
CapabilityTypeName::Runner => "runner",
CapabilityTypeName::Resolver => "resolver",
};
write!(f, "{}", display_name)
}
}
impl Into<component_internal::AllowlistedCapability> for CapabilityTypeName {
fn into(self) -> component_internal::AllowlistedCapability {
match &self {
CapabilityTypeName::Directory => component_internal::AllowlistedCapability::Directory(
component_internal::AllowlistedDirectory::default(),
),
CapabilityTypeName::Protocol => component_internal::AllowlistedCapability::Protocol(
component_internal::AllowlistedProtocol::default(),
),
CapabilityTypeName::Service => component_internal::AllowlistedCapability::Service(
component_internal::AllowlistedService::default(),
),
CapabilityTypeName::Storage => component_internal::AllowlistedCapability::Storage(
component_internal::AllowlistedStorage::default(),
),
CapabilityTypeName::Runner => component_internal::AllowlistedCapability::Runner(
component_internal::AllowlistedRunner::default(),
),
CapabilityTypeName::Resolver => component_internal::AllowlistedCapability::Resolver(
component_internal::AllowlistedResolver::default(),
),
}
}
}
#[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::default(),
)
}
}
}
}
#[derive(Deserialize, Debug, Clone, PartialEq, Hash, Eq, Ord, PartialOrd)]
#[serde(rename_all = "lowercase")]
pub enum CapabilityFrom {
Capability,
Component,
Framework,
}
impl Into<fdecl::Ref> for CapabilityFrom {
fn into(self) -> fdecl::Ref {
match &self {
CapabilityFrom::Capability => {
fdecl::Ref::Capability(fdecl::CapabilityRef { name: "".into() })
}
CapabilityFrom::Component => fdecl::Ref::Self_(fdecl::SelfRef {}),
CapabilityFrom::Framework => fdecl::Ref::Framework(fdecl::FrameworkRef {}),
}
}
}
#[derive(Deserialize, Debug, Default, PartialEq, Clone, Ord, PartialOrd, Eq)]
#[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>>,
}
impl CapabilityAllowlistEntry {
fn merge_vecs(
some: Vec<CapabilityAllowlistEntry>,
another: Vec<CapabilityAllowlistEntry>,
) -> Result<Vec<CapabilityAllowlistEntry>, Error> {
#[derive(Hash, Eq, PartialEq)]
struct Source {
source_moniker: Option<String>,
source_name: Option<String>,
source: Option<CapabilityFrom>,
capability: Option<CapabilityTypeName>,
}
let mut combined: Vec<_> = some
.into_iter()
.chain(another.into_iter())
.map(|mut entry| {
(
Source {
source_moniker: entry.source_moniker.take(),
source_name: entry.source_name.take(),
source: entry.source.take(),
capability: entry.capability.take(),
},
entry.target_monikers,
)
})
.into_grouping_map()
.fold_first(|accumulated, _key, item| {
{
merge_option(accumulated, item, |mut l, mut r| {
l.append(&mut r);
Ok(l)
})
}
.unwrap()
})
.into_iter()
.map(|(mut key, values)| CapabilityAllowlistEntry {
source_moniker: key.source_moniker.take(),
source_name: key.source_name.take(),
source: key.source.take(),
capability: key.capability.take(),
target_monikers: match values {
Some(v) => Some(remove_duplicates(v)),
None => None,
},
})
.collect();
// Unstable sorts are faster, and we shouldn't have items that have equal values anyway.
combined.sort_unstable();
Ok(combined)
}
}
#[derive(Deserialize, Debug, Default)]
#[serde(deny_unknown_fields)]
pub struct DebugRegistrationAllowlistEntry {
name: Option<String>,
debug: Option<DebugRegistrationTypeName>,
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_namespace_capability(capability, &mut used_ids)?;
}
}
// Validate "builtin_capabilities".
if let Some(capabilities) = config.builtin_capabilities.as_ref() {
let mut used_ids = HashSet::new();
for capability in capabilities {
Config::validate_builtin_capability(capability, &mut used_ids)?;
}
}
Ok(Self {
debug: config.debug,
enable_introspection: config.enable_introspection,
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)),
namespace_capabilities: config
.namespace_capabilities
.as_ref()
.map(|c| {
cml::translate::translate_capabilities(&CompileOptions::default(), c, false)
})
.transpose()?,
builtin_capabilities: config
.builtin_capabilities
.as_ref()
.map(|c| {
cml::translate::translate_capabilities(&CompileOptions::default(), c, true)
})
.transpose()?,
num_threads: config.num_threads,
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_destination: config.log_destination.map(|d| d.into()),
log_all_events: config.log_all_events,
builtin_boot_resolver: config.builtin_boot_resolver.map(Into::into),
realm_builder_resolver_and_runner: config
.realm_builder_resolver_and_runner
.map(Into::into),
abi_revision_policy: config.abi_revision_policy.map(Into::into),
vmex_source: config.vmex_source.map(Into::into),
..Default::default()
})
}
}
// 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, child_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),
child_policy: child_policy.map(translate_child_policy),
..Default::default()
}
}
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,
..Default::default()
}
}
fn translate_child_policy(
child_policy: ChildPolicyAllowlists,
) -> component_internal::ChildPolicyAllowlists {
component_internal::ChildPolicyAllowlists {
reboot_on_terminate: child_policy.reboot_on_terminate,
..Default::default()
}
}
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(),
..Default::default()
})
.collect::<Vec<_>>();
component_internal::CapabilityPolicyAllowlists {
allowlist: Some(allowlist),
..Default::default()
}
}
fn translate_debug_registration_policy(
debug_registration_policy: Vec<DebugRegistrationAllowlistEntry>,
) -> component_internal::DebugRegistrationPolicyAllowlists {
let allowlist = debug_registration_policy
.iter()
.map(|e| component_internal::DebugRegistrationAllowlistEntry {
name: e.name.clone(),
debug: e.debug.clone().map_or_else(|| None, |t| Some(t.into())),
moniker: e.moniker.clone(),
environment_name: e.environment_name.clone(),
..Default::default()
})
.collect::<Vec<_>>();
component_internal::DebugRegistrationPolicyAllowlists {
allowlist: Some(allowlist),
..Default::default()
}
}
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 merge(self, another: Config) -> Result<Self, Error> {
Ok(Config {
debug: merge_field!(self, another, debug),
enable_introspection: merge_field!(self, another, enable_introspection),
use_builtin_process_launcher: merge_field!(self, another, use_builtin_process_launcher),
maintain_utc_clock: merge_field!(self, another, maintain_utc_clock),
list_children_batch_size: merge_field!(self, another, list_children_batch_size),
security_policy: deep_merge_field!(self, another, security_policy),
namespace_capabilities: merge_field!(self, another, namespace_capabilities),
builtin_capabilities: merge_field!(self, another, builtin_capabilities),
num_threads: merge_field!(self, another, num_threads),
root_component_url: merge_field!(self, another, root_component_url),
component_id_index_path: merge_field!(self, another, component_id_index_path),
log_destination: merge_field!(self, another, log_destination),
log_all_events: merge_field!(self, another, log_all_events),
builtin_boot_resolver: merge_field!(self, another, builtin_boot_resolver),
realm_builder_resolver_and_runner: merge_field!(
self,
another,
realm_builder_resolver_and_runner
),
abi_revision_policy: merge_field!(self, another, abi_revision_policy),
vmex_source: merge_field!(self, another, vmex_source),
})
}
fn validate_namespace_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(())
}
fn validate_builtin_capability(
capability: &cml::Capability,
used_ids: &mut HashSet<String>,
) -> Result<(), Error> {
if capability.storage.is_some() {
return Err(Error::validate("\"storage\" is not supported for built-in capabilities"));
}
if capability.directory.is_some() && capability.rights.is_none() {
return Err(Error::validate("\"rights\" should be present with \"directory\""));
}
if capability.path.is_some() {
return Err(Error::validate(
"\"path\" should not be present for built-in 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(Deserialize, Debug, Default)]
#[serde(deny_unknown_fields)]
struct ProductConfig {
#[serde(default)]
ambient_mark_vmo_exec: AmbientMarkVmoExec,
#[serde(default)]
platform_capabilities: PlatformCapabilities,
#[serde(default)]
product_capabilities: ProductCapabilities,
}
#[derive(Deserialize, Debug, Default)]
#[serde(deny_unknown_fields)]
struct AmbientMarkVmoExec {
session: Vec<String>,
}
#[derive(Deserialize, Debug, Default)]
#[serde(deny_unknown_fields)]
struct PlatformCapabilities {
session: Vec<PlatformCapabilityEntry>,
}
#[derive(Deserialize, Debug, Default)]
#[serde(deny_unknown_fields)]
struct ProductCapabilities {
session: Vec<ProductCapabilityEntry>,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct PlatformCapabilityEntry {
capability: PlatformCapability,
target_monikers: Vec<String>,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct ProductCapabilityEntry {
source_moniker: String,
capability: String,
target_monikers: Vec<String>,
}
impl ProductConfig {
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))
})
}
}
impl TryInto<Config> for ProductConfig {
type Error = Error;
fn try_into(self) -> Result<Config, Error> {
let Self { ambient_mark_vmo_exec, platform_capabilities, product_capabilities } = self;
fn rebase_string(s: String) -> String {
format!("{SESSION_MONIKER}/{s}")
}
fn rebase_vec_string(vec: Vec<String>) -> Vec<String> {
vec.into_iter().map(|s| rebase_string(s)).collect()
}
let vmo_exec =
ambient_mark_vmo_exec.session.into_iter().map(|s| rebase_string(s)).collect();
let job_policy =
JobPolicyAllowlists { ambient_mark_vmo_exec: Some(vmo_exec), ..Default::default() };
let platform_capabilities =
platform_capabilities.session.into_iter().map(|entry| CapabilityAllowlistEntry {
source: Some(CapabilityFrom::Component),
capability: Some(CapabilityTypeName::Protocol),
source_moniker: Some(entry.capability.source_moniker().to_string()),
source_name: Some(entry.capability.source_name().to_string()),
target_monikers: Some(rebase_vec_string(entry.target_monikers)),
});
let capability_policy = product_capabilities
.session
.into_iter()
.map(|entry| CapabilityAllowlistEntry {
source: Some(CapabilityFrom::Component),
capability: Some(CapabilityTypeName::Protocol),
source_moniker: Some(rebase_string(entry.source_moniker)),
source_name: Some(entry.capability),
target_monikers: Some(rebase_vec_string(entry.target_monikers)),
})
.chain(platform_capabilities)
.collect();
Ok(Config {
security_policy: Some(SecurityPolicy {
job_policy: Some(job_policy),
capability_policy: Some(capability_policy),
..Default::default()
}),
..Default::default()
})
}
}
/// Create a binary config and populate it with data from .json file.
#[derive(Debug, Default, FromArgs)]
pub struct Args {
/// path to a JSON configuration file
#[argh(option)]
pub input: Vec<PathBuf>,
/// path to a product-specific JSON configuration file
#[argh(option)]
pub product: Vec<PathBuf>,
/// path to the output binary config file
#[argh(option)]
pub output: PathBuf,
}
/// Compile multiple platform configs with an optional product config, and generate a file that can
/// be added to BootFS for component_manager to use at runtime.
pub fn compile(args: Args) -> Result<(), Error> {
let configs =
args.input.iter().map(Config::from_json_file).collect::<Result<Vec<Config>, _>>()?;
let mut config_json =
configs.into_iter().try_fold(Config::default(), |acc, next| acc.merge(next))?;
for product in args.product.into_iter() {
let product = ProductConfig::from_json_file(&product)?;
config_json = config_json.merge(product.try_into()?)?;
}
let config_fidl: component_internal::Config = config_json.try_into()?;
let bytes = persist(&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 {
assert_matches::assert_matches, fidl::unpersist, fidl_fuchsia_component_decl as fdecl,
fidl_fuchsia_io as fio, 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], product: vec![] };
compile(args)?;
let mut bytes = Vec::new();
File::open(output_path)?.read_to_end(&mut bytes)?;
let config: component_internal::Config = unpersist(&bytes)?;
Ok(config)
}
#[test]
fn test_compile() {
let input = r#"{
debug: true,
enable_introspection: true,
list_children_batch_size: 123,
maintain_utc_clock: false,
use_builtin_process_launcher: true,
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.boot.RootResource",
capability: "protocol",
target_monikers: ["/root", "/root/bootstrap", "/root/core"],
},
],
debug_registration_policy: [
{
name: "fuchsia.boot.RootResource",
debug: "protocol",
moniker: "/foo",
environment_name: "my_env",
},
],
child_policy: {
reboot_on_terminate: [ "/buz" ],
},
},
namespace_capabilities: [
{
protocol: "foo_svc",
},
{
directory: "bar_dir",
path: "/bar",
rights: [ "connect" ],
},
],
builtin_capabilities: [
{
protocol: "foo_protocol",
},
{
directory: "foo_dir",
rights: [ "connect" ],
},
{
service: "foo_svc",
},
{
runner: "foo_runner",
},
{
resolver: "foo_resolver",
},
{
event_stream: "foo_event_stream",
}
],
num_threads: 321,
root_component_url: "fuchsia-pkg://fuchsia.com/foo#meta/foo.cm",
component_id_index_path: "/this/is/an/absolute/path",
log_destination: "klog",
log_all_events: true,
builtin_boot_resolver: "boot",
realm_builder_resolver_and_runner: "namespace",
vmex_source: "namespace",
}"#;
let config = compile_str(input).expect("failed to compile");
assert_eq!(
config,
component_internal::Config {
debug: Some(true),
enable_introspection: Some(true),
maintain_utc_clock: Some(false),
use_builtin_process_launcher: Some(true),
list_children_batch_size: Some(123),
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()]),
..Default::default()
}),
capability_policy: Some(component_internal::CapabilityPolicyAllowlists {
allowlist: Some(vec![component_internal::CapabilityAllowlistEntry {
source_moniker: Some("<component_manager>".to_string()),
source_name: Some("fuchsia.boot.RootResource".to_string()),
source: Some(fdecl::Ref::Self_(fdecl::SelfRef {})),
capability: Some(component_internal::AllowlistedCapability::Protocol(
component_internal::AllowlistedProtocol::default()
)),
target_monikers: Some(vec![
"/root".to_string(),
"/root/bootstrap".to_string(),
"/root/core".to_string()
]),
..Default::default()
},]),
..Default::default()
}),
debug_registration_policy: Some(
component_internal::DebugRegistrationPolicyAllowlists {
allowlist: Some(vec![
component_internal::DebugRegistrationAllowlistEntry {
name: Some("fuchsia.boot.RootResource".to_string()),
debug: Some(
component_internal::AllowlistedDebugRegistration::Protocol(
component_internal::AllowlistedProtocol::default()
)
),
moniker: Some("/foo".to_string()),
environment_name: Some("my_env".to_string()),
..Default::default()
}
]),
..Default::default()
}
),
child_policy: Some(component_internal::ChildPolicyAllowlists {
reboot_on_terminate: Some(vec!["/buz".to_string()]),
..Default::default()
}),
..Default::default()
}),
namespace_capabilities: Some(vec![
fdecl::Capability::Protocol(fdecl::Protocol {
name: Some("foo_svc".into()),
source_path: Some("/svc/foo_svc".into()),
..Default::default()
}),
fdecl::Capability::Directory(fdecl::Directory {
name: Some("bar_dir".into()),
source_path: Some("/bar".into()),
rights: Some(fio::Operations::CONNECT),
..Default::default()
}),
]),
builtin_capabilities: Some(vec![
fdecl::Capability::Protocol(fdecl::Protocol {
name: Some("foo_protocol".into()),
source_path: None,
..Default::default()
}),
fdecl::Capability::Directory(fdecl::Directory {
name: Some("foo_dir".into()),
source_path: None,
rights: Some(fio::Operations::CONNECT),
..Default::default()
}),
fdecl::Capability::Service(fdecl::Service {
name: Some("foo_svc".into()),
source_path: None,
..Default::default()
}),
fdecl::Capability::Runner(fdecl::Runner {
name: Some("foo_runner".into()),
source_path: None,
..Default::default()
}),
fdecl::Capability::Resolver(fdecl::Resolver {
name: Some("foo_resolver".into()),
source_path: None,
..Default::default()
}),
fdecl::Capability::EventStream(fdecl::EventStream {
name: Some("foo_event_stream".into()),
..Default::default()
}),
]),
num_threads: Some(321),
root_component_url: Some("fuchsia-pkg://fuchsia.com/foo#meta/foo.cm".to_string()),
component_id_index_path: Some("/this/is/an/absolute/path".to_string()),
log_destination: Some(component_internal::LogDestination::Klog),
log_all_events: Some(true),
builtin_boot_resolver: Some(component_internal::BuiltinBootResolver::Boot),
realm_builder_resolver_and_runner: Some(
component_internal::RealmBuilderResolverAndRunner::Namespace
),
vmex_source: Some(component_internal::VmexSource::Namespace),
..Default::default()
}
);
}
#[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 { 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 { 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 { err, .. } )
if &err == "\"path\" should be present with \"directory\""
);
}
}
#[test]
fn test_validate_builtin_capabilities() {
{
let input = r#"{
builtin_capabilities: [
{
protocol: "foo",
},
{
directory: "foo",
rights: [ "connect" ],
},
],
}"#;
assert_matches!(
compile_str(input),
Err(Error::Validate { err, .. } )
if &err == "\"foo\" is a duplicate \"capability\" name"
);
}
{
let input = r#"{
builtin_capabilities: [
{
directory: "foo",
path: "/foo",
},
],
}"#;
assert_matches!(
compile_str(input),
Err(Error::Validate { err, .. } )
if &err == "\"rights\" should be present with \"directory\""
);
}
{
let input = r#"{
builtin_capabilities: [
{
storage: "foo",
},
],
}"#;
assert_matches!(
compile_str(input),
Err(Error::Validate { err, .. } )
if &err == "\"storage\" is not supported for built-in capabilities"
);
}
{
let input = r#"{
builtin_capabilities: [
{
runner: "foo",
path: "/foo",
},
],
}"#;
assert_matches!(
compile_str(input),
Err(Error::Validate { err, .. } )
if &err == "\"path\" should not be present for built-in capabilities"
);
}
}
#[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],
product: vec![],
};
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],
product: vec![],
};
compile(args)?;
let mut bytes = Vec::new();
File::open(output_path)?.read_to_end(&mut bytes)?;
let config: component_internal::Config = unpersist(&bytes)?;
assert_eq!(config.debug, Some(true));
assert_eq!(config.list_children_batch_size, Some(42));
Ok(())
}
#[test]
fn test_deep_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 = r#"{
security_policy: {
job_policy: {
ambient_mark_vmo_exec: ["/foo1"],
create_raw_processes: ["/foo1"],
},
},
}"#;
File::create(&input_path).unwrap().write_all(input.as_bytes()).unwrap();
let another_input_path = tmp_dir.path().join("bar.json");
let another_input = r#"{
security_policy: {
job_policy: {
ambient_mark_vmo_exec: ["/foo2"],
create_raw_processes: ["/foo2"],
},
},
}"#;
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],
product: vec![],
};
compile(args)?;
let mut bytes = Vec::new();
File::open(output_path)?.read_to_end(&mut bytes)?;
let config: component_internal::Config = unpersist(&bytes)?;
assert_eq!(
config.security_policy,
Some(component_internal::SecurityPolicy {
job_policy: Some(component_internal::JobPolicyAllowlists {
ambient_mark_vmo_exec: Some(vec!["/foo1".to_string(), "/foo2".to_string()]),
create_raw_processes: Some(vec!["/foo1".to_string(), "/foo2".to_string()]),
..Default::default()
}),
..Default::default()
})
);
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 { .. }));
}
#[test]
fn test_capability_allowlist_merging() {
let left = vec![
CapabilityAllowlistEntry {
source_moniker: Some("moniker1".into()),
source_name: Some("name1".into()),
source: Some(CapabilityFrom::Capability),
capability: Some(CapabilityTypeName::Protocol),
target_monikers: Some(vec!["target1".into()]),
},
CapabilityAllowlistEntry {
source_moniker: Some("moniker2".into()),
source_name: Some("name2".into()),
source: Some(CapabilityFrom::Capability),
capability: Some(CapabilityTypeName::Protocol),
target_monikers: Some(vec!["target2".into()]),
},
];
let right = vec![
CapabilityAllowlistEntry {
source_moniker: Some("moniker2".into()),
source_name: Some("name2".into()),
source: Some(CapabilityFrom::Capability),
capability: Some(CapabilityTypeName::Protocol),
target_monikers: Some(vec!["target3".into()]),
},
CapabilityAllowlistEntry {
source_moniker: Some("moniker1".into()),
source_name: Some("name1".into()),
source: Some(CapabilityFrom::Capability),
capability: Some(CapabilityTypeName::Protocol),
target_monikers: Some(vec!["target4".into()]),
},
];
let expected_combine = vec![
CapabilityAllowlistEntry {
source_moniker: Some("moniker1".into()),
source_name: Some("name1".into()),
source: Some(CapabilityFrom::Capability),
capability: Some(CapabilityTypeName::Protocol),
target_monikers: Some(vec!["target1".into(), "target4".into()]),
},
CapabilityAllowlistEntry {
source_moniker: Some("moniker2".into()),
source_name: Some("name2".into()),
source: Some(CapabilityFrom::Capability),
capability: Some(CapabilityTypeName::Protocol),
target_monikers: Some(vec!["target2".into(), "target3".into()]),
},
];
let combined = CapabilityAllowlistEntry::merge_vecs(left, right).unwrap();
assert_eq!(combined, expected_combine);
}
#[test]
fn test_capability_allowlist_merging_no_dups() {
let left = vec![
CapabilityAllowlistEntry {
source_moniker: Some("moniker1".into()),
source_name: Some("name1".into()),
source: Some(CapabilityFrom::Capability),
capability: Some(CapabilityTypeName::Protocol),
target_monikers: Some(vec!["target1".into()]),
},
CapabilityAllowlistEntry {
source_moniker: Some("moniker2".into()),
source_name: Some("name2".into()),
source: Some(CapabilityFrom::Capability),
capability: Some(CapabilityTypeName::Protocol),
target_monikers: Some(vec!["target2".into()]),
},
];
let right = left.clone();
let expected_combine = left.clone();
let combined = CapabilityAllowlistEntry::merge_vecs(left, right).unwrap();
assert_eq!(combined, expected_combine);
}
#[test]
fn test_product_and_platform() -> 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 = r#"{
security_policy: {
job_policy: {
ambient_mark_vmo_exec: ["/foo1"],
create_raw_processes: ["/foo1"],
},
capability_policy: [
{
source_moniker: "<component_manager>",
source: "component",
source_name: "fuchsia.boot.RootResource",
capability: "protocol",
target_monikers: ["/root", "/root/bootstrap", "/root/core"],
},
],
},
}"#;
File::create(&input_path).unwrap().write_all(input.as_bytes()).unwrap();
let another_input_path = tmp_dir.path().join("bar.json");
let another_input = r#"{
ambient_mark_vmo_exec: {
session: ["foo2"],
},
platform_capabilities: {
session: [
{
capability: "fuchsia.lowpan.device.DeviceExtraConnector",
target_monikers: ["session_comp1"],
},
],
},
product_capabilities: {
session: [
{
source_moniker: "session_comp1",
capability: "product.some.Resource",
target_monikers: ["session_comp2", "session_comp3"],
},
],
},
}"#;
File::create(&another_input_path).unwrap().write_all(another_input.as_bytes()).unwrap();
let args = Args {
output: output_path.clone(),
input: vec![input_path],
product: vec![another_input_path],
};
compile(args)?;
let mut bytes = Vec::new();
File::open(output_path)?.read_to_end(&mut bytes)?;
let config: component_internal::Config = unpersist(&bytes)?;
assert_eq!(
config.security_policy,
Some(component_internal::SecurityPolicy {
job_policy: Some(component_internal::JobPolicyAllowlists {
ambient_mark_vmo_exec: Some(vec![
"/core/session-manager/session:session/foo2".to_string(),
"/foo1".to_string()
]),
create_raw_processes: Some(vec!["/foo1".to_string()]),
..Default::default()
}),
capability_policy: Some(component_internal::CapabilityPolicyAllowlists {
allowlist: Some(vec![
component_internal::CapabilityAllowlistEntry {
source_moniker: Some("/core/lowpanservice".to_string()),
source_name: Some(
"fuchsia.lowpan.device.DeviceExtraConnector".to_string()
),
source: Some(fdecl::Ref::Self_(fdecl::SelfRef {})),
capability: Some(component_internal::AllowlistedCapability::Protocol(
component_internal::AllowlistedProtocol::default()
)),
target_monikers: Some(vec![
"/core/session-manager/session:session/session_comp1".to_string(),
]),
..Default::default()
},
component_internal::CapabilityAllowlistEntry {
source_moniker: Some(
"/core/session-manager/session:session/session_comp1".to_string()
),
source_name: Some("product.some.Resource".to_string()),
source: Some(fdecl::Ref::Self_(fdecl::SelfRef {})),
capability: Some(component_internal::AllowlistedCapability::Protocol(
component_internal::AllowlistedProtocol::default()
)),
target_monikers: Some(vec![
"/core/session-manager/session:session/session_comp2".to_string(),
"/core/session-manager/session:session/session_comp3".to_string(),
]),
..Default::default()
},
component_internal::CapabilityAllowlistEntry {
source_moniker: Some("<component_manager>".to_string()),
source_name: Some("fuchsia.boot.RootResource".to_string()),
source: Some(fdecl::Ref::Self_(fdecl::SelfRef {})),
capability: Some(component_internal::AllowlistedCapability::Protocol(
component_internal::AllowlistedProtocol::default()
)),
target_monikers: Some(vec![
"/root".to_string(),
"/root/bootstrap".to_string(),
"/root/core".to_string()
]),
..Default::default()
},
]),
..Default::default()
}),
..Default::default()
})
);
Ok(())
}
#[test]
fn test_product_only() -> Result<(), Error> {
let tmp_dir = TempDir::new().unwrap();
let output_path = tmp_dir.path().join("config");
let another_input_path = tmp_dir.path().join("bar.json");
let another_input = r#"{
ambient_mark_vmo_exec: {
session: ["foo2"],
},
platform_capabilities: {
session: [
{
capability: "fuchsia.lowpan.device.DeviceExtraConnector",
target_monikers: ["session_comp1"],
},
],
},
product_capabilities: {
session: [
{
source_moniker: "session_comp1",
capability: "product.some.Resource",
target_monikers: ["session_comp2", "session_comp3"],
},
],
},
}"#;
File::create(&another_input_path).unwrap().write_all(another_input.as_bytes()).unwrap();
let args =
Args { output: output_path.clone(), input: vec![], product: vec![another_input_path] };
compile(args)?;
let mut bytes = Vec::new();
File::open(output_path)?.read_to_end(&mut bytes)?;
let config: component_internal::Config = unpersist(&bytes)?;
assert_eq!(
config.security_policy,
Some(component_internal::SecurityPolicy {
job_policy: Some(component_internal::JobPolicyAllowlists {
ambient_mark_vmo_exec: Some(vec![
"/core/session-manager/session:session/foo2".to_string(),
]),
..Default::default()
}),
capability_policy: Some(component_internal::CapabilityPolicyAllowlists {
allowlist: Some(vec![
component_internal::CapabilityAllowlistEntry {
source_moniker: Some(
"/core/session-manager/session:session/session_comp1".to_string()
),
source_name: Some("product.some.Resource".to_string()),
source: Some(fdecl::Ref::Self_(fdecl::SelfRef {})),
capability: Some(component_internal::AllowlistedCapability::Protocol(
component_internal::AllowlistedProtocol::default()
)),
target_monikers: Some(vec![
"/core/session-manager/session:session/session_comp2".to_string(),
"/core/session-manager/session:session/session_comp3".to_string(),
]),
..Default::default()
},
component_internal::CapabilityAllowlistEntry {
source_moniker: Some("/core/lowpanservice".to_string()),
source_name: Some(
"fuchsia.lowpan.device.DeviceExtraConnector".to_string()
),
source: Some(fdecl::Ref::Self_(fdecl::SelfRef {})),
capability: Some(component_internal::AllowlistedCapability::Protocol(
component_internal::AllowlistedProtocol::default()
)),
target_monikers: Some(vec![
"/core/session-manager/session:session/session_comp1".to_string(),
]),
..Default::default()
},
]),
..Default::default()
}),
..Default::default()
})
);
Ok(())
}
#[test]
fn test_product_invalid_platform_capability() {
let tmp_dir = TempDir::new().unwrap();
let output_path = tmp_dir.path().join("config");
let another_input_path = tmp_dir.path().join("bar.json");
let another_input = r#"{
platform_capabilities: {
session: [
{
capability: "fuchsia.nonexistent.Resource",
target_monikers: ["session_comp1"],
},
],
},
}"#;
File::create(&another_input_path).unwrap().write_all(another_input.as_bytes()).unwrap();
let args =
Args { output: output_path.clone(), input: vec![], product: vec![another_input_path] };
assert_matches!(compile(args), Err(Error::Parse { .. }));
}
}