| // 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::Context; |
| use camino::{Utf8Path, Utf8PathBuf}; |
| use clonable_error::ClonableError; |
| use fidl_fuchsia_component_internal as fcomponent_internal; |
| use moniker::Moniker; |
| use std::collections::{HashMap, HashSet}; |
| use thiserror::Error; |
| |
| #[cfg(feature = "serde")] |
| use serde::{Deserialize, Serialize}; |
| |
| pub mod fidl_convert; |
| mod instance_id; |
| |
| pub use instance_id::{InstanceId, InstanceIdError}; |
| |
| /// Component ID index entry, only used for persistence to JSON5 and FIDL.. |
| #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] |
| #[derive(Debug, PartialEq, Eq, Clone)] |
| pub(crate) struct PersistedIndexEntry { |
| pub instance_id: InstanceId, |
| pub moniker: Moniker, |
| } |
| |
| /// Component ID index, only used for persistence to JSON5 and FIDL. |
| /// |
| /// Unlike [Index], this type is not validated, so may contain duplicate monikers |
| /// and instance IDs. |
| #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] |
| #[derive(Debug, PartialEq, Eq, Clone)] |
| pub(crate) struct PersistedIndex { |
| instances: Vec<PersistedIndexEntry>, |
| } |
| |
| /// An index that maps component monikers to instance IDs. |
| /// |
| /// Unlike [PersistedIndex], this type is validated to only contain unique instance IDs. |
| #[cfg_attr( |
| feature = "serde", |
| derive(Deserialize, Serialize), |
| serde(try_from = "PersistedIndex", into = "PersistedIndex") |
| )] |
| #[derive(Debug, PartialEq, Eq, Clone)] |
| pub struct Index { |
| /// Map of a moniker from the index to its instance ID. |
| moniker_to_instance_id: HashMap<Moniker, InstanceId>, |
| |
| /// All instance IDs, equivalent to the values of `moniker_to_instance_id`. |
| instance_ids: HashSet<InstanceId>, |
| } |
| |
| #[derive(Error, Clone, Debug)] |
| pub enum IndexError { |
| #[error("failed to read index file '{path}'")] |
| ReadFile { |
| #[source] |
| err: ClonableError, |
| path: Utf8PathBuf, |
| }, |
| #[error("invalid index")] |
| ValidationError(#[from] ValidationError), |
| #[error("could not merge indices")] |
| MergeError(#[from] MergeError), |
| #[error("could not convert FIDL index")] |
| FidlConversionError(#[from] fidl_convert::FidlConversionError), |
| } |
| |
| impl Index { |
| /// Return an Index parsed from the FIDL file at `path`. |
| pub fn from_fidl_file(path: &Utf8Path) -> Result<Self, IndexError> { |
| fn fidl_index_from_file( |
| path: &Utf8Path, |
| ) -> Result<fcomponent_internal::ComponentIdIndex, anyhow::Error> { |
| let raw_content = std::fs::read(path).context("failed to read file")?; |
| let fidl_index = fidl::unpersist::<fcomponent_internal::ComponentIdIndex>(&raw_content) |
| .context("failed to unpersist FIDL")?; |
| Ok(fidl_index) |
| } |
| let fidl_index = fidl_index_from_file(path) |
| .map_err(|err| IndexError::ReadFile { err: err.into(), path: path.to_owned() })?; |
| let index = Index::try_from(fidl_index)?; |
| Ok(index) |
| } |
| |
| /// Construct an Index by merging JSON5 source files. |
| #[cfg(feature = "serde")] |
| pub fn merged_from_json5_files(paths: &[Utf8PathBuf]) -> Result<Self, IndexError> { |
| fn index_from_json5_file(path: &Utf8Path) -> Result<Index, anyhow::Error> { |
| let mut file = std::fs::File::open(&path).context("failed to open")?; |
| let index: Index = serde_json5::from_reader(&mut file).context("failed to parse")?; |
| Ok(index) |
| } |
| let mut ctx = MergeContext::default(); |
| for path in paths { |
| let index = index_from_json5_file(path) |
| .map_err(|err| IndexError::ReadFile { err: err.into(), path: path.to_owned() })?; |
| ctx.merge(path, &index)?; |
| } |
| Ok(ctx.output()) |
| } |
| |
| /// Insert an entry into the index. |
| pub fn insert( |
| &mut self, |
| moniker: Moniker, |
| instance_id: InstanceId, |
| ) -> Result<(), ValidationError> { |
| if !self.instance_ids.insert(instance_id.clone()) { |
| return Err(ValidationError::DuplicateId(instance_id.clone())); |
| } |
| if self.moniker_to_instance_id.insert(moniker.clone(), instance_id).is_some() { |
| return Err(ValidationError::DuplicateMoniker(moniker)); |
| } |
| Ok(()) |
| } |
| |
| /// Returns the instance ID for the moniker, if the index contains the moniker. |
| pub fn id_for_moniker(&self, moniker: &Moniker) -> Option<&InstanceId> { |
| self.moniker_to_instance_id.get(&moniker) |
| } |
| |
| /// Returns true if the index contains the instance ID. |
| pub fn contains_id(&self, id: &InstanceId) -> bool { |
| self.instance_ids.contains(id) |
| } |
| } |
| |
| impl Default for Index { |
| fn default() -> Self { |
| Index { moniker_to_instance_id: HashMap::new(), instance_ids: HashSet::new() } |
| } |
| } |
| |
| impl TryFrom<PersistedIndex> for Index { |
| type Error = ValidationError; |
| |
| fn try_from(value: PersistedIndex) -> Result<Self, Self::Error> { |
| let mut index = Index::default(); |
| for entry in value.instances.into_iter() { |
| index.insert(entry.moniker, entry.instance_id)?; |
| } |
| Ok(index) |
| } |
| } |
| |
| impl From<Index> for PersistedIndex { |
| fn from(value: Index) -> Self { |
| let mut instances = value |
| .moniker_to_instance_id |
| .into_iter() |
| .map(|(moniker, instance_id)| PersistedIndexEntry { instance_id, moniker }) |
| .collect::<Vec<_>>(); |
| instances.sort_by(|a, b| a.moniker.cmp(&b.moniker)); |
| Self { instances } |
| } |
| } |
| |
| #[derive(Error, Debug, Clone, PartialEq)] |
| pub enum ValidationError { |
| #[error("duplicate moniker: {}", .0)] |
| DuplicateMoniker(Moniker), |
| #[error("duplicate instance ID: {}", .0)] |
| DuplicateId(InstanceId), |
| } |
| |
| #[derive(Error, Debug, Clone, PartialEq)] |
| pub enum MergeError { |
| #[error("Moniker {}' must be unique but exists in following index files:\n {}\n {}", .moniker, .source1, .source2)] |
| DuplicateMoniker { moniker: Moniker, source1: Utf8PathBuf, source2: Utf8PathBuf }, |
| #[error("Instance ID '{}' must be unique but exists in following index files:\n {}\n {}", .instance_id, .source1, .source2)] |
| DuplicateId { instance_id: InstanceId, source1: Utf8PathBuf, source2: Utf8PathBuf }, |
| } |
| |
| /// A builder that merges indices into a single accumulated index. |
| pub struct MergeContext { |
| /// Index that contains entries accumulated from calls to [`merge()`]. |
| output_index: Index, |
| // Path to the source index file that contains the moniker. |
| moniker_to_source_path: HashMap<Moniker, Utf8PathBuf>, |
| // Path to the source index file that contains the instance ID. |
| instance_id_to_source_path: HashMap<InstanceId, Utf8PathBuf>, |
| } |
| |
| impl MergeContext { |
| // Merge `index` into the into the MergeContext. |
| // |
| // This method can be called multiple times to merge multiple indices. |
| // The resulting index can be accessed with output(). |
| pub fn merge(&mut self, source_index_path: &Utf8Path, index: &Index) -> Result<(), MergeError> { |
| for (moniker, instance_id) in &index.moniker_to_instance_id { |
| self.output_index.insert(moniker.clone(), instance_id.clone()).map_err( |
| |err| match err { |
| ValidationError::DuplicateMoniker(moniker) => { |
| let previous_source_path = |
| self.moniker_to_source_path.get(&moniker).cloned().unwrap_or_default(); |
| MergeError::DuplicateMoniker { |
| moniker, |
| source1: previous_source_path, |
| source2: source_index_path.to_owned(), |
| } |
| } |
| ValidationError::DuplicateId(instance_id) => { |
| let previous_source_path = self |
| .instance_id_to_source_path |
| .get(&instance_id) |
| .cloned() |
| .unwrap_or_default(); |
| MergeError::DuplicateId { |
| instance_id, |
| source1: previous_source_path, |
| source2: source_index_path.to_owned(), |
| } |
| } |
| }, |
| )?; |
| self.instance_id_to_source_path |
| .insert(instance_id.clone(), source_index_path.to_owned()); |
| self.moniker_to_source_path.insert(moniker.clone(), source_index_path.to_owned()); |
| } |
| Ok(()) |
| } |
| |
| // Return the accumulated index from calls to merge(). |
| pub fn output(self) -> Index { |
| self.output_index |
| } |
| } |
| |
| impl Default for MergeContext { |
| fn default() -> Self { |
| MergeContext { |
| output_index: Index::default(), |
| instance_id_to_source_path: HashMap::new(), |
| moniker_to_source_path: HashMap::new(), |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use anyhow::Result; |
| |
| #[test] |
| fn merge_empty_index() { |
| let ctx = MergeContext::default(); |
| assert_eq!(ctx.output(), Index::default()); |
| } |
| |
| #[test] |
| fn merge_single_index() -> Result<()> { |
| let mut ctx = MergeContext::default(); |
| |
| let mut index = Index::default(); |
| let moniker = vec!["foo"].try_into().unwrap(); |
| let instance_id = InstanceId::new_random(&mut rand::thread_rng()); |
| index.insert(moniker, instance_id).unwrap(); |
| |
| ctx.merge(Utf8Path::new("/random/file/path"), &index)?; |
| assert_eq!(ctx.output(), index.clone()); |
| Ok(()) |
| } |
| |
| #[test] |
| fn merge_duplicate_id() -> Result<()> { |
| let source1 = Utf8Path::new("/a/b/c"); |
| let source2 = Utf8Path::new("/d/e/f"); |
| |
| let id = InstanceId::new_random(&mut rand::thread_rng()); |
| let index1 = { |
| let mut index = Index::default(); |
| let moniker = vec!["foo"].try_into().unwrap(); |
| index.insert(moniker, id.clone()).unwrap(); |
| index |
| }; |
| let index2 = { |
| let mut index = Index::default(); |
| let moniker = vec!["bar"].try_into().unwrap(); |
| index.insert(moniker, id.clone()).unwrap(); |
| index |
| }; |
| |
| let mut ctx = MergeContext::default(); |
| ctx.merge(source1, &index1)?; |
| |
| let err = ctx.merge(source2, &index2).unwrap_err(); |
| assert_eq!( |
| err, |
| MergeError::DuplicateId { |
| instance_id: id, |
| source1: source1.to_owned(), |
| source2: source2.to_owned() |
| } |
| ); |
| |
| Ok(()) |
| } |
| |
| #[test] |
| fn merge_duplicate_moniker() -> Result<()> { |
| let source1 = Utf8Path::new("/a/b/c"); |
| let source2 = Utf8Path::new("/d/e/f"); |
| |
| let moniker: Moniker = vec!["foo"].try_into().unwrap(); |
| let index1 = { |
| let mut index = Index::default(); |
| let id = InstanceId::new_random(&mut rand::thread_rng()); |
| index.insert(moniker.clone(), id).unwrap(); |
| index |
| }; |
| let index2 = { |
| let mut index = Index::default(); |
| let id = InstanceId::new_random(&mut rand::thread_rng()); |
| index.insert(moniker.clone(), id).unwrap(); |
| index |
| }; |
| |
| let mut ctx = MergeContext::default(); |
| ctx.merge(source1, &index1)?; |
| |
| let err = ctx.merge(source2, &index2).unwrap_err(); |
| assert_eq!( |
| err, |
| MergeError::DuplicateMoniker { |
| moniker, |
| source1: source1.to_owned(), |
| source2: source2.to_owned() |
| } |
| ); |
| |
| Ok(()) |
| } |
| |
| #[cfg(feature = "serde")] |
| #[test] |
| fn merged_from_json5_files() { |
| use std::io::Write; |
| |
| let mut index_file_1 = tempfile::NamedTempFile::new().unwrap(); |
| index_file_1 |
| .write_all( |
| r#"{ |
| // Here is a comment. |
| instances: [ |
| { |
| instance_id: "fb94044d62278b37c221c7fdeebdcf1304262f3e11416f68befa5ef88b7a2163", |
| moniker: "/a/b" |
| } |
| ] |
| }"# |
| .as_bytes(), |
| ) |
| .unwrap(); |
| |
| let mut index_file_2 = tempfile::NamedTempFile::new().unwrap(); |
| index_file_2 |
| .write_all( |
| r#"{ |
| // Here is a comment. |
| instances: [ |
| { |
| instance_id: "4f915af6c4b682867ab7ad2dc9cbca18342ddd9eec61724f19d231cf6d07f122", |
| moniker: "/c/d" |
| } |
| ] |
| }"# |
| .as_bytes(), |
| ) |
| .unwrap(); |
| |
| let expected_index = { |
| let mut index = Index::default(); |
| index |
| .insert( |
| "/a/b".parse::<Moniker>().unwrap(), |
| "fb94044d62278b37c221c7fdeebdcf1304262f3e11416f68befa5ef88b7a2163" |
| .parse::<InstanceId>() |
| .unwrap(), |
| ) |
| .unwrap(); |
| index |
| .insert( |
| "/c/d".parse::<Moniker>().unwrap(), |
| "4f915af6c4b682867ab7ad2dc9cbca18342ddd9eec61724f19d231cf6d07f122" |
| .parse::<InstanceId>() |
| .unwrap(), |
| ) |
| .unwrap(); |
| index |
| }; |
| |
| // only checking that we parsed successfully. |
| let files = [ |
| Utf8PathBuf::from_path_buf(index_file_1.path().to_path_buf()).unwrap(), |
| Utf8PathBuf::from_path_buf(index_file_2.path().to_path_buf()).unwrap(), |
| ]; |
| assert_eq!(expected_index, Index::merged_from_json5_files(&files).unwrap()); |
| } |
| |
| #[cfg(feature = "serde")] |
| #[test] |
| fn serialize_deserialize() -> Result<()> { |
| let expected_index = { |
| let mut index = Index::default(); |
| for i in 0..5 { |
| let moniker: Moniker = vec![i.to_string().as_str()].try_into().unwrap(); |
| let instance_id = InstanceId::new_random(&mut rand::thread_rng()); |
| index.insert(moniker, instance_id).unwrap(); |
| } |
| index |
| }; |
| |
| let json_index = serde_json5::to_string(&expected_index)?; |
| let actual_index = serde_json5::from_str(&json_index)?; |
| assert_eq!(expected_index, actual_index); |
| |
| Ok(()) |
| } |
| } |