| // 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. |
| |
| // This library must remain platform-agnostic because it used by a host tool and within Fuchsia. |
| |
| use anyhow::{anyhow, Context, Result}; |
| use serde::{Deserialize, Serialize}; |
| use std::collections::HashMap; |
| use std::fs; |
| use std::str; |
| use thiserror::Error; |
| |
| pub mod fidl_convert; |
| |
| #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] |
| pub struct AppmgrMoniker { |
| pub url: String, |
| pub realm_path: Vec<String>, |
| pub transitional_realm_paths: Option<Vec<Vec<String>>>, |
| } |
| |
| #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] |
| pub struct InstanceIdEntry { |
| pub instance_id: Option<String>, |
| pub appmgr_moniker: Option<AppmgrMoniker>, |
| pub moniker: Option<String>, |
| } |
| |
| #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] |
| pub struct Index { |
| pub appmgr_restrict_isolated_persistent_storage: Option<bool>, |
| pub instances: Vec<InstanceIdEntry>, |
| } |
| |
| impl Index { |
| // Construct an Index by merging index source files. |
| // |
| // - `index_file_paths` is a list of component ID index file paths to be validated and merged. |
| // - `decoder` is a function which decodes (without schema-validating) an index file content into an Index. |
| // |
| // See `ValidationError` for possible errors. |
| pub fn from_files_with_decoder( |
| index_file_paths: &[String], |
| decoder: impl Fn(&str) -> anyhow::Result<Index>, |
| ) -> anyhow::Result<Index> { |
| let mut ctx = MergeContext::new(); |
| for input_file_path in index_file_paths { |
| let contents = fs::read_to_string(&input_file_path) |
| .with_context(|| anyhow!("Could not read index file {}", &input_file_path))?; |
| let index = decoder(contents.as_str()) |
| .with_context(|| anyhow!("Could not parse index file {}", &input_file_path))?; |
| ctx.merge(&input_file_path, &index) |
| .with_context(|| anyhow!("Could not merge index file {}", &input_file_path))?; |
| } |
| Ok(ctx.output()) |
| } |
| } |
| |
| #[derive(Error, Debug, PartialEq)] |
| pub enum ValidationError { |
| #[error("Instance ID '{}' must be unique but exists in following index files:\n {}\n {}", .instance_id, .source1, .source2)] |
| DuplicateIds { instance_id: String, source1: String, source2: String }, |
| #[error("Some entries do not specify an instance ID.")] |
| MissingInstanceIds { entries: Vec<InstanceIdEntry> }, |
| #[error("The following entry must contain atleast an appmgr_moniker or an moniker: {:?}", .entry)] |
| MissingMoniker { entry: InstanceIdEntry }, |
| #[error("The following entry's instance_id is invalid (must be 64 lower-cased hex chars): {:?}", .entry)] |
| InvalidInstanceId { entry: InstanceIdEntry }, |
| #[error("appmgr_restrict_isolated_persistent_storage has already been set to {} and cannot be set twice.", .previous_val)] |
| MultipleStorageRestrictions { previous_val: bool }, |
| } |
| |
| // MergeContext maintains a single merged index, along with some state for error checking, as indicies are merged together using MergeContext::merge(). |
| // |
| // Usage: |
| // - Use MergeContext::new() to create a MergeContext. |
| // - Call MergeContext::merge() to merge an index. Can be called multiple times. |
| // - Call MergeContext::output() to access the merged index. |
| struct MergeContext { |
| output_index: Index, |
| // MergeConetext::merge() will accumulate the instance IDs which have been merged so far, along with the index source file which they came from. |
| // This is used to validate that all instance IDs are unique and provide helpful error messages. |
| // instance id -> path of file defining instance ID. |
| accumulated_instance_ids: HashMap<String, String>, |
| } |
| |
| impl MergeContext { |
| fn new() -> MergeContext { |
| MergeContext { |
| output_index: Index { |
| appmgr_restrict_isolated_persistent_storage: None, |
| instances: vec![], |
| }, |
| accumulated_instance_ids: HashMap::new(), |
| } |
| } |
| |
| // merge() merges `index` into the MergeContext. |
| // This method can be called multiple times to merge multiple indicies. |
| // The accumulated index can be accessed with output(). |
| fn merge(&mut self, source_index_path: &str, index: &Index) -> Result<(), ValidationError> { |
| let mut missing_instance_ids = vec![]; |
| for entry in &index.instances { |
| match entry.instance_id.as_ref() { |
| None => { |
| // Instead of failing right away, continue processing the other entries. |
| missing_instance_ids.push(entry.clone()); |
| continue; |
| } |
| Some(instance_id) => { |
| if !is_valid_instance_id(&instance_id) { |
| return Err(ValidationError::InvalidInstanceId { entry: entry.clone() }); |
| } |
| if let Some(previous_source_path) = self |
| .accumulated_instance_ids |
| .insert(instance_id.clone(), source_index_path.to_string()) |
| { |
| return Err(ValidationError::DuplicateIds { |
| instance_id: instance_id.clone(), |
| source1: previous_source_path.clone(), |
| source2: source_index_path.to_string(), |
| }); |
| } |
| } |
| } |
| if entry.appmgr_moniker.is_none() && entry.moniker.is_none() { |
| return Err(ValidationError::MissingMoniker { entry: entry.clone() }); |
| } |
| self.output_index.instances.push(entry.clone()); |
| } |
| if let Some(val) = index.appmgr_restrict_isolated_persistent_storage { |
| if let Some(previous_val) = |
| self.output_index.appmgr_restrict_isolated_persistent_storage |
| { |
| return Err(ValidationError::MultipleStorageRestrictions { previous_val }); |
| } else { |
| self.output_index.appmgr_restrict_isolated_persistent_storage = Some(val); |
| } |
| } |
| if missing_instance_ids.len() > 0 { |
| Err(ValidationError::MissingInstanceIds { entries: missing_instance_ids }) |
| } else { |
| Ok(()) |
| } |
| } |
| |
| // Access the accumulated index from calls to merge(). |
| fn output(self) -> Index { |
| self.output_index |
| } |
| } |
| |
| // Generate a random instance ID. |
| pub fn gen_instance_id(rng: &mut impl rand::Rng) -> String { |
| // generate random 256bits into a byte array |
| let mut num: [u8; 256 / 8] = [0; 256 / 8]; |
| rng.fill_bytes(&mut num); |
| // turn the byte array into a lower-cased hex string. |
| num.iter().map(|byte| format!("{:02x}", byte)).collect::<Vec<String>>().join("") |
| } |
| |
| fn is_valid_instance_id(id: &str) -> bool { |
| // An instance ID is a lower-cased hex string of 256-bits. |
| // 256 bits in base16 = 64 chars (1 char to represent 4 bits) |
| id.len() == 64 && id.chars().all(|ch| (ch.is_numeric() || ch.is_lowercase()) && ch.is_digit(16)) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use proptest::prelude::*; |
| use rand::*; |
| |
| fn gen_index(num_instances: u32) -> Index { |
| Index { |
| appmgr_restrict_isolated_persistent_storage: None, |
| instances: (0..num_instances) |
| .map(|i| InstanceIdEntry { |
| instance_id: Some(gen_instance_id(&mut rand::thread_rng())), |
| appmgr_moniker: Some(AppmgrMoniker { |
| url: format!( |
| "fuchsia-pkg://example.com/fake_pkg#meta/fake_component_{}.cmx", |
| i |
| ), |
| realm_path: vec!["root".to_string(), "child".to_string(), i.to_string()], |
| transitional_realm_paths: None, |
| }), |
| moniker: None, |
| }) |
| .collect(), |
| } |
| } |
| |
| #[test] |
| fn merge_empty_index() { |
| let ctx = MergeContext::new(); |
| assert_eq!(ctx.output(), gen_index(0)); |
| } |
| |
| #[test] |
| fn merge_single_index() { |
| let mut ctx = MergeContext::new(); |
| let index = gen_index(0); |
| ctx.merge("/random/file/path", &index).unwrap(); |
| assert_eq!(ctx.output(), index.clone()); |
| } |
| |
| #[test] |
| fn merge_duplicate_ids() { |
| let source1 = "/a/b/c"; |
| let source2 = "/d/e/f"; |
| |
| let index1 = gen_index(1); |
| let mut index2 = index1.clone(); |
| index2.instances[0].instance_id = index1.instances[0].instance_id.clone(); |
| |
| let mut ctx = MergeContext::new(); |
| ctx.merge(source1, &index1).unwrap(); |
| |
| let err = ctx.merge(source2, &index2).unwrap_err(); |
| |
| assert_eq!( |
| err, |
| ValidationError::DuplicateIds { |
| instance_id: index1.instances[0].instance_id.as_ref().unwrap().clone(), |
| source1: source1.to_string(), |
| source2: source2.to_string() |
| } |
| ); |
| } |
| |
| #[test] |
| fn multiple_appmgr_restrict_isolated_persistent_storage() { |
| let source1 = "/a/b/c"; |
| let source2 = "/d/e/f"; |
| |
| let mut index1 = gen_index(0); |
| index1.appmgr_restrict_isolated_persistent_storage = Some(true); |
| let mut index2 = index1.clone(); |
| index2.appmgr_restrict_isolated_persistent_storage = Some(false); |
| |
| let mut ctx = MergeContext::new(); |
| ctx.merge(source1, &index1).unwrap(); |
| let err = ctx.merge(source2, &index2).unwrap_err(); |
| |
| assert_eq!(err, ValidationError::MultipleStorageRestrictions { previous_val: true }); |
| } |
| |
| #[test] |
| fn missing_instance_ids() { |
| let mut index = gen_index(4); |
| index.instances[1].instance_id = None; |
| index.instances[3].instance_id = None; |
| |
| let mut ctx = MergeContext::new(); |
| // this should be an error, since `index` has entries with a missing instance ID. |
| let merge_result: Result<(), ValidationError> = ctx.merge("/a/b/c", &index); |
| assert!(matches!( |
| merge_result.as_ref(), |
| Err(ValidationError::MissingInstanceIds { entries: _ }) |
| )); |
| } |
| |
| #[test] |
| fn missing_moniker() { |
| let mut index = gen_index(1); |
| index.instances[0].appmgr_moniker = None; |
| index.instances[0].moniker = None; |
| |
| let mut ctx = MergeContext::new(); |
| // this should be an error, since `index` has an entry without any monikers. |
| let merge_result: Result<(), ValidationError> = ctx.merge("/a/b/c", &index); |
| assert!(matches!(merge_result.as_ref(), Err(ValidationError::MissingMoniker { entry: _ }))); |
| } |
| |
| #[test] |
| fn abs_moniker_and_appmgr_moniker() { |
| let mut index = gen_index(1); |
| // this entry has both an `appmgr_moniker` *and* a `moniker`. |
| assert!(index.instances[0].appmgr_moniker.is_some()); |
| index.instances[0].moniker = Some("/a/b/c".to_string()); |
| |
| let mut ctx = MergeContext::new(); |
| let merge_result: Result<(), ValidationError> = ctx.merge("/a/b/c", &index); |
| assert!(merge_result.is_ok()); |
| } |
| |
| #[test] |
| fn unique_gen_instance_id() { |
| let seed = rand::thread_rng().next_u64(); |
| println!("using seed {}", seed); |
| let mut rng = rand::rngs::StdRng::seed_from_u64(seed); |
| let mut prev_id = gen_instance_id(&mut rng); |
| for _i in 0..40 { |
| let id = gen_instance_id(&mut rng); |
| assert!(prev_id != id); |
| prev_id = id; |
| } |
| } |
| |
| #[test] |
| fn valid_gen_instance_id() { |
| let seed = rand::thread_rng().next_u64(); |
| println!("using seed {}", seed); |
| let mut rng = rand::rngs::StdRng::seed_from_u64(seed); |
| for _i in 0..40 { |
| assert!(is_valid_instance_id(&gen_instance_id(&mut rng))); |
| } |
| } |
| |
| proptest! { |
| #[test] |
| fn valid_instance_id(id in "[a-f0-9]{64}") { |
| prop_assert_eq!(true, is_valid_instance_id(&id)); |
| } |
| } |
| |
| #[test] |
| fn invalid_instance_id() { |
| // Invalid lengths |
| assert!(!is_valid_instance_id("8c90d44863ff67586cf6961081feba4f760decab8bbbee376a3bfbc77")); |
| assert!(!is_valid_instance_id("8c90d44863ff67586cf6961081feba4f760decab8bbbee376a")); |
| assert!(!is_valid_instance_id("8c90d44863ff67586cf6961081")); |
| // upper case chars are invalid |
| assert!(!is_valid_instance_id( |
| "8C90D44863FF67586CF6961081FEBA4F760DECAB8BBBEE376A3BFBC77B351280" |
| )); |
| // hex chars only |
| assert!(!is_valid_instance_id( |
| "8x90d44863ff67586cf6961081feba4f760decab8bbbee376a3bfbc77b351280" |
| )); |
| assert!(!is_valid_instance_id( |
| "8;90d44863ff67586cf6961081feba4f760decab8bbbee376a3bfbc77b351280" |
| )); |
| } |
| } |