[component_id_index] Realms can look up instance IDs

Specifically:
* component manager gets a path to the component ID index through
  RuntimeConfig.component_id_index_path
* ModelContext parses & builds a ComponentIdIndex from
  RuntimeConfig.component_id_index_path

Storage routing can now lookup instance_id when figuring out directory
structure (done in followup CL).

Change-Id: I2903ba00299eb431d4286c29fe8413e05393351d
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/460023
Commit-Queue: Vardhan Mudunuru <vardhan@google.com>
Reviewed-by: Gary Bressler <geb@google.com>
API-Review: Gary Bressler <geb@google.com>
Testability-Review: Gary Bressler <geb@google.com>
diff --git a/sdk/fidl/fuchsia.component.internal/config.fidl b/sdk/fidl/fuchsia.component.internal/config.fidl
index 9fb3632a..405ae07 100644
--- a/sdk/fidl/fuchsia.component.internal/config.fidl
+++ b/sdk/fidl/fuchsia.component.internal/config.fidl
@@ -60,6 +60,10 @@
     /// is passed to component manager. If value is passed in both places, then
     /// an error is raised.
     10: string:fuchsia.component.MAX_URL_SCHEME_LENGTH root_component_url;
+
+    /// Path to the component ID index. An empty value defaults to an empty index.
+    /// An invalid index causes component_manager to abort.
+    11: string:fuchsia.component.MAX_PATH_LENGTH component_id_index_path;
 };
 
 /// The builtin resolver to use, if any.
diff --git a/src/sys/component_manager/BUILD.gn b/src/sys/component_manager/BUILD.gn
index 5ccebccbe..03a93f0 100644
--- a/src/sys/component_manager/BUILD.gn
+++ b/src/sys/component_manager/BUILD.gn
@@ -48,6 +48,7 @@
     "//src/sys/lib/cm_fidl_validator",
     "//src/sys/lib/cm_rust",
     "//src/sys/lib/cm_types",
+    "//src/sys/lib/component_id_index",
     "//src/sys/lib/directory_broker",
     "//src/sys/lib/moniker",
     "//src/sys/lib/runner",
@@ -115,6 +116,7 @@
     "src/model/actions/stop.rs",
     "src/model/addable_directory.rs",
     "src/model/binding.rs",
+    "src/model/component_id_index.rs",
     "src/model/context.rs",
     "src/model/dir_tree.rs",
     "src/model/environment.rs",
diff --git a/src/sys/component_manager/src/builtin_environment.rs b/src/sys/component_manager/src/builtin_environment.rs
index 5e40959..bc7f82c 100644
--- a/src/sys/component_manager/src/builtin_environment.rs
+++ b/src/sys/component_manager/src/builtin_environment.rs
@@ -240,7 +240,7 @@
             runtime_config: Arc::clone(&runtime_config),
             namespace_capabilities: runtime_config.namespace_capabilities.clone(),
         };
-        let model = Arc::new(Model::new(params));
+        let model = Arc::new(Model::new(params).await?);
 
         // If we previously created a resolver that requires the Model (in
         // add_available_resolvers_from_namespace), send the just-created model to it.
diff --git a/src/sys/component_manager/src/config.rs b/src/sys/component_manager/src/config.rs
index c15536a..5ab0619 100644
--- a/src/sys/component_manager/src/config.rs
+++ b/src/sys/component_manager/src/config.rs
@@ -74,6 +74,10 @@
     /// is passed to component manager. If value is passed in both places, then
     /// an error is raised.
     pub root_component_url: Option<Url>,
+
+    /// Path to the component ID index, parsed from
+    /// `fuchsia.component.internal.RuntimeConfig.component_id_index_path`.
+    pub component_id_index_path: Option<String>,
 }
 
 /// Runtime security policy.
@@ -142,6 +146,7 @@
             builtin_pkg_resolver: BuiltinPkgResolver::None,
             out_dir_contents: OutDirContents::None,
             root_component_url: Default::default(),
+            component_id_index_path: None,
         }
     }
 }
@@ -247,6 +252,7 @@
                 .unwrap_or(default.builtin_pkg_resolver),
             out_dir_contents: config.out_dir_contents.unwrap_or(default.out_dir_contents),
             root_component_url,
+            component_id_index_path: config.component_id_index_path,
         })
     }
 }
@@ -397,6 +403,7 @@
             builtin_pkg_resolver: None,
             out_dir_contents: None,
             root_component_url: None,
+            component_id_index_path: None,
             ..component_internal::Config::EMPTY
         }, RuntimeConfig::default()),
         all_leaf_nodes_none => (component_internal::Config {
@@ -418,6 +425,7 @@
             namespace_capabilities: None,
             out_dir_contents: None,
             root_component_url: None,
+            component_id_index_path: None,
             ..component_internal::Config::EMPTY
         }, RuntimeConfig {
             debug:false, list_children_batch_size: 5,
@@ -482,6 +490,7 @@
                 ]),
                 out_dir_contents: Some(component_internal::OutDirContents::Svc),
                 root_component_url: Some(FOO_PKG_URL.to_string()),
+                component_id_index_path: Some("/boot/config/component_id_index".to_string()),
                 ..component_internal::Config::EMPTY
             },
             RuntimeConfig {
@@ -540,6 +549,7 @@
                 builtin_pkg_resolver: BuiltinPkgResolver::None,
                 out_dir_contents: OutDirContents::Svc,
                 root_component_url: Some(Url::new(FOO_PKG_URL.to_string()).unwrap()),
+                component_id_index_path: Some("/boot/config/component_id_index".to_string()),
             }
         ),
     }
@@ -564,6 +574,7 @@
             namespace_capabilities: None,
             out_dir_contents: None,
             root_component_url: None,
+            component_id_index_path: None,
             ..component_internal::Config::EMPTY
         }, MonikerError, MonikerError::InvalidMoniker {rep: "bad".to_string()}),
         invalid_capability_policy_empty_allowlist_cap => (component_internal::Config {
@@ -592,6 +603,7 @@
             namespace_capabilities: None,
             out_dir_contents: None,
             root_component_url: None,
+            component_id_index_path: None,
         ..component_internal::Config::EMPTY
     }, PolicyConfigError, PolicyConfigError::EmptyAllowlistedCapability),
     invalid_capability_policy_empty_source_moniker => (component_internal::Config {
@@ -619,6 +631,7 @@
         namespace_capabilities: None,
         out_dir_contents: None,
         root_component_url: None,
+        component_id_index_path: None,
         ..component_internal::Config::EMPTY
     }, PolicyConfigError, PolicyConfigError::EmptySourceMoniker),
     invalid_root_component_url => (component_internal::Config {
@@ -632,6 +645,7 @@
         namespace_capabilities: None,
         out_dir_contents: None,
         root_component_url: Some("invalid url".to_string()),
+        component_id_index_path: None,
         ..component_internal::Config::EMPTY
     }, ParseError, ParseError::InvalidValue),
     }
diff --git a/src/sys/component_manager/src/model/component_id_index.rs b/src/sys/component_manager/src/model/component_id_index.rs
new file mode 100644
index 0000000..bad147b
--- /dev/null
+++ b/src/sys/component_manager/src/model/component_id_index.rs
@@ -0,0 +1,152 @@
+// 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 {
+    clonable_error::ClonableError,
+    component_id_index, fidl_fuchsia_component_internal as fcomponent_internal, io_util,
+    moniker::{AbsoluteMoniker, MonikerError},
+    std::collections::HashMap,
+    thiserror::Error,
+};
+
+pub type ComponentInstanceId = String;
+
+#[derive(Debug, Clone, Error)]
+pub enum ComponentIdIndexError {
+    #[error("could not read index file {}", .path)]
+    IndexUnreadable {
+        #[source]
+        err: ClonableError,
+        path: String,
+    },
+    #[error("Index error")]
+    IndexError(#[from] component_id_index::IndexError),
+    #[error("invalid moniker")]
+    MonikerError(#[from] MonikerError),
+}
+
+/// ComponentIdIndex parses a given index and provides methods to look up instance IDs.
+#[derive(Debug, Default)]
+pub struct ComponentIdIndex {
+    _moniker_to_instance_id: HashMap<AbsoluteMoniker, ComponentInstanceId>,
+}
+
+impl ComponentIdIndex {
+    pub async fn new(index_file_path: &str) -> Result<Self, ComponentIdIndexError> {
+        let fidl_index = io_util::file::read_in_namespace_to_fidl::<
+            fcomponent_internal::ComponentIdIndex,
+        >(index_file_path)
+        .await
+        .map_err(|err| ComponentIdIndexError::IndexUnreadable {
+            path: index_file_path.to_string(),
+            err: err.into(),
+        })?;
+
+        let index = component_id_index::Index::from_fidl(fidl_index)?;
+
+        let mut moniker_to_instance_id = HashMap::<AbsoluteMoniker, ComponentInstanceId>::new();
+        for entry in &index.instances {
+            if let Some(moniker) = &entry.moniker {
+                let absolute_moniker = AbsoluteMoniker::parse_string_without_instances(moniker)
+                    .map_err(|e| ComponentIdIndexError::MonikerError(e))?;
+                moniker_to_instance_id.insert(
+                    absolute_moniker,
+                    entry
+                        .instance_id
+                        .as_ref()
+                        .ok_or_else(|| {
+                            ComponentIdIndexError::IndexError(
+                                component_id_index::IndexError::ValidationError(
+                                    component_id_index::ValidationError::MissingInstanceIds {
+                                        entries: vec![entry.clone()],
+                                    },
+                                ),
+                            )
+                        })?
+                        .clone(),
+                );
+            }
+        }
+        Ok(Self { _moniker_to_instance_id: moniker_to_instance_id })
+    }
+
+    pub fn look_up_moniker(&self, moniker: &AbsoluteMoniker) -> Option<&ComponentInstanceId> {
+        self._moniker_to_instance_id.get(moniker)
+    }
+}
+
+#[cfg(test)]
+pub mod tests {
+    use super::*;
+    use anyhow::Result;
+    use fidl::encoding::encode_persistent;
+    use fuchsia_async as fasync;
+    use std::convert::TryFrom;
+    use std::io::Write;
+    use tempfile::NamedTempFile;
+
+    fn make_index_file(index: component_id_index::Index) -> Result<NamedTempFile> {
+        let mut tmp_file = NamedTempFile::new()?;
+        tmp_file.write_all(
+            encode_persistent(&mut fcomponent_internal::ComponentIdIndex::try_from(index)?)?
+                .as_ref(),
+        )?;
+        Ok(tmp_file)
+    }
+
+    #[fasync::run_singlethreaded(test)]
+    async fn invalid_moniker() {
+        let index_file = make_index_file(component_id_index::Index {
+            instances: vec![component_id_index::InstanceIdEntry {
+                instance_id: Some("0".repeat(64)),
+                appmgr_moniker: None,
+                moniker: Some("invalid moniker".to_string()),
+            }],
+            ..component_id_index::Index::default()
+        })
+        .unwrap();
+
+        let moniker = ComponentIdIndex::new(index_file.path().to_str().unwrap()).await;
+        assert!(matches!(
+            moniker.err().unwrap(),
+            ComponentIdIndexError::MonikerError(MonikerError::InvalidMoniker { rep: _ }),
+        ));
+    }
+
+    #[fasync::run_singlethreaded(test)]
+    async fn look_up_moniker_no_exists() {
+        let index_file = make_index_file(component_id_index::Index::default()).unwrap();
+        let index = ComponentIdIndex::new(index_file.path().to_str().unwrap()).await.unwrap();
+        assert!(index
+            .look_up_moniker(&AbsoluteMoniker::parse_string_without_instances("/a/b/c").unwrap())
+            .is_none());
+    }
+
+    #[fasync::run_singlethreaded(test)]
+    async fn look_up_moniker_exists() {
+        let iid = "0".repeat(64);
+        let index_file = make_index_file(component_id_index::Index {
+            instances: vec![component_id_index::InstanceIdEntry {
+                instance_id: Some(iid.clone()),
+                appmgr_moniker: None,
+                moniker: Some("/a/b/c".to_string()),
+            }],
+            ..component_id_index::Index::default()
+        })
+        .unwrap();
+        let index = ComponentIdIndex::new(index_file.path().to_str().unwrap()).await.unwrap();
+        assert_eq!(
+            Some(&iid),
+            index.look_up_moniker(
+                &AbsoluteMoniker::parse_string_without_instances("/a/b/c").unwrap()
+            )
+        );
+    }
+
+    #[fasync::run_singlethreaded(test)]
+    async fn index_unreadable() {
+        let result = ComponentIdIndex::new("/this/path/doesnt/exist").await;
+        assert!(matches!(result, Err(ComponentIdIndexError::IndexUnreadable { path: _, err: _ })));
+    }
+}
diff --git a/src/sys/component_manager/src/model/context.rs b/src/sys/component_manager/src/model/context.rs
index 2d93d34..32cff8c 100644
--- a/src/sys/component_manager/src/model/context.rs
+++ b/src/sys/component_manager/src/model/context.rs
@@ -5,7 +5,9 @@
 use {
     crate::{
         config::RuntimeConfig,
-        model::{error::ModelError, policy::GlobalPolicyChecker},
+        model::{
+            component_id_index::ComponentIdIndex, error::ModelError, policy::GlobalPolicyChecker,
+        },
     },
     std::sync::{Arc, Weak},
 };
@@ -15,18 +17,29 @@
 /// want to share with Realms.
 pub struct ModelContext {
     policy_checker: GlobalPolicyChecker,
+    component_id_index: ComponentIdIndex,
 }
 
 impl ModelContext {
     /// Constructs a new ModelContext from a RuntimeConfig.
-    pub fn new(runtime_config: Arc<RuntimeConfig>) -> Self {
-        Self { policy_checker: GlobalPolicyChecker::new(runtime_config) }
+    pub async fn new(runtime_config: Arc<RuntimeConfig>) -> Result<Self, ModelError> {
+        Ok(Self {
+            component_id_index: match &runtime_config.component_id_index_path {
+                Some(path) => ComponentIdIndex::new(&path).await?,
+                None => ComponentIdIndex::default(),
+            },
+            policy_checker: GlobalPolicyChecker::new(runtime_config),
+        })
     }
 
     /// Returns the runtime policy checker for the model.
     pub fn policy(&self) -> &GlobalPolicyChecker {
         &self.policy_checker
     }
+
+    pub fn component_id_index(&self) -> &ComponentIdIndex {
+        &self.component_id_index
+    }
 }
 
 /// A wrapper for a weak reference to `ModelContext`. It implements an upgrade()
diff --git a/src/sys/component_manager/src/model/environment.rs b/src/sys/component_manager/src/model/environment.rs
index dc43ef5..a0027be 100644
--- a/src/sys/component_manager/src/model/environment.rs
+++ b/src/sys/component_manager/src/model/environment.rs
@@ -311,12 +311,16 @@
             registry
         };
 
-        let model = Arc::new(Model::new(ModelParams {
-            runtime_config: Arc::new(RuntimeConfig::default()),
-            root_component_url: "test:///root".to_string(),
-            root_environment: Environment::new_root(RunnerRegistry::new(runners), resolvers),
-            namespace_capabilities: vec![],
-        }));
+        let model = Arc::new(
+            Model::new(ModelParams {
+                runtime_config: Arc::new(RuntimeConfig::default()),
+                root_component_url: "test:///root".to_string(),
+                root_environment: Environment::new_root(RunnerRegistry::new(runners), resolvers),
+                namespace_capabilities: vec![],
+            })
+            .await
+            .unwrap(),
+        );
         let realm = model.bind(&vec!["a:0", "b:0"].into(), &BindReason::Eager).await?;
         assert_eq!(realm.component_url, "test:///b");
 
@@ -374,12 +378,16 @@
             registry
         };
 
-        let model = Arc::new(Model::new(ModelParams {
-            runtime_config: Arc::new(RuntimeConfig::default()),
-            root_component_url: "test:///root".to_string(),
-            root_environment: Environment::new_root(RunnerRegistry::new(runners), resolvers),
-            namespace_capabilities: vec![],
-        }));
+        let model = Arc::new(
+            Model::new(ModelParams {
+                runtime_config: Arc::new(RuntimeConfig::default()),
+                root_component_url: "test:///root".to_string(),
+                root_environment: Environment::new_root(RunnerRegistry::new(runners), resolvers),
+                namespace_capabilities: vec![],
+            })
+            .await
+            .unwrap(),
+        );
         let realm = model.bind(&vec!["a:0", "b:0"].into(), &BindReason::Eager).await?;
         assert_eq!(realm.component_url, "test:///b");
 
@@ -441,12 +449,16 @@
             registry
         };
 
-        let model = Arc::new(Model::new(ModelParams {
-            runtime_config: Arc::new(RuntimeConfig::default()),
-            root_component_url: "test:///root".to_string(),
-            root_environment: Environment::new_root(RunnerRegistry::new(runners), resolvers),
-            namespace_capabilities: vec![],
-        }));
+        let model = Arc::new(
+            Model::new(ModelParams {
+                runtime_config: Arc::new(RuntimeConfig::default()),
+                root_component_url: "test:///root".to_string(),
+                root_environment: Environment::new_root(RunnerRegistry::new(runners), resolvers),
+                namespace_capabilities: vec![],
+            })
+            .await
+            .unwrap(),
+        );
         // Add instance to collection.
         {
             let parent_realm = model.bind(&vec!["a:0"].into(), &BindReason::Eager).await?;
@@ -506,12 +518,16 @@
             registry
         };
 
-        let model = Arc::new(Model::new(ModelParams {
-            runtime_config: Arc::new(RuntimeConfig::default()),
-            root_component_url: "test:///root".to_string(),
-            root_environment: Environment::new_root(RunnerRegistry::new(runners), resolvers),
-            namespace_capabilities: vec![],
-        }));
+        let model = Arc::new(
+            Model::new(ModelParams {
+                runtime_config: Arc::new(RuntimeConfig::default()),
+                root_component_url: "test:///root".to_string(),
+                root_environment: Environment::new_root(RunnerRegistry::new(runners), resolvers),
+                namespace_capabilities: vec![],
+            })
+            .await
+            .unwrap(),
+        );
 
         let realm = model.bind(&vec!["a:0", "b:0"].into(), &BindReason::Eager).await?;
         assert_eq!(realm.component_url, "test:///b");
@@ -557,12 +573,16 @@
             registry.register("test".to_string(), Box::new(resolver)).unwrap();
             registry
         };
-        let model = Arc::new(Model::new(ModelParams {
-            runtime_config: Arc::new(RuntimeConfig::default()),
-            root_component_url: "test:///root".to_string(),
-            root_environment: Environment::new_root(RunnerRegistry::default(), registry),
-            namespace_capabilities: vec![],
-        }));
+        let model = Arc::new(
+            Model::new(ModelParams {
+                runtime_config: Arc::new(RuntimeConfig::default()),
+                root_component_url: "test:///root".to_string(),
+                root_environment: Environment::new_root(RunnerRegistry::default(), registry),
+                namespace_capabilities: vec![],
+            })
+            .await
+            .unwrap(),
+        );
         assert_matches!(
             model.bind(&vec!["a:0", "b:0"].into(), &BindReason::Eager).await,
             Err(ModelError::ResolverError { .. })
diff --git a/src/sys/component_manager/src/model/error.rs b/src/sys/component_manager/src/model/error.rs
index 2d812e6..1bbd30e 100644
--- a/src/sys/component_manager/src/model/error.rs
+++ b/src/sys/component_manager/src/model/error.rs
@@ -4,9 +4,9 @@
 
 use {
     crate::model::{
-        environment::EnvironmentError, events::error::EventsError, policy::PolicyError,
-        resolver::ResolverError, rights::RightsError, routing::RoutingError, runner::RunnerError,
-        storage::StorageError,
+        component_id_index::ComponentIdIndexError, environment::EnvironmentError,
+        events::error::EventsError, policy::PolicyError, resolver::ResolverError,
+        rights::RightsError, routing::RoutingError, runner::RunnerError, storage::StorageError,
     },
     anyhow::Error,
     clonable_error::ClonableError,
@@ -30,6 +30,11 @@
     CollectionNotFound { name: String },
     #[error("context not found")]
     ContextNotFound,
+    #[error("component id index invalid: {}", err)]
+    ComponentIdIndexError {
+        #[from]
+        err: ComponentIdIndexError,
+    },
     #[error("environment {} not found in realm {}", name, moniker)]
     EnvironmentNotFound { name: String, moniker: AbsoluteMoniker },
     #[error("environment {} in realm {} is not valid: {}", name, moniker, err)]
diff --git a/src/sys/component_manager/src/model/events/source_factory.rs b/src/sys/component_manager/src/model/events/source_factory.rs
index 23a11b8..980bd99 100644
--- a/src/sys/component_manager/src/model/events/source_factory.rs
+++ b/src/sys/component_manager/src/model/events/source_factory.rs
@@ -266,15 +266,19 @@
                 registry.register("test".to_string(), Box::new(resolver)).unwrap();
                 registry
             };
-            Arc::new(Model::new(ModelParams {
-                runtime_config: Arc::new(RuntimeConfig::default()),
-                root_component_url: "test:///root".to_string(),
-                root_environment: Environment::new_root(
-                    RunnerRegistry::default(),
-                    resolver_registry,
-                ),
-                namespace_capabilities: vec![],
-            }))
+            Arc::new(
+                Model::new(ModelParams {
+                    runtime_config: Arc::new(RuntimeConfig::default()),
+                    root_component_url: "test:///root".to_string(),
+                    root_environment: Environment::new_root(
+                        RunnerRegistry::default(),
+                        resolver_registry,
+                    ),
+                    namespace_capabilities: vec![],
+                })
+                .await
+                .unwrap(),
+            )
         };
         let event_registry = Arc::new(EventRegistry::new(Arc::downgrade(&model)));
         let event_source_factory = Arc::new(EventSourceFactory::new(
@@ -301,12 +305,16 @@
     async fn passes_on_capability_routed_from_framework_not_on_root() {
         let model = {
             let resolver = ResolverRegistry::new();
-            Arc::new(Model::new(ModelParams {
-                runtime_config: Arc::new(RuntimeConfig::default()),
-                root_component_url: "test:///root".to_string(),
-                root_environment: Environment::new_root(RunnerRegistry::default(), resolver),
-                namespace_capabilities: vec![],
-            }))
+            Arc::new(
+                Model::new(ModelParams {
+                    runtime_config: Arc::new(RuntimeConfig::default()),
+                    root_component_url: "test:///root".to_string(),
+                    root_environment: Environment::new_root(RunnerRegistry::default(), resolver),
+                    namespace_capabilities: vec![],
+                })
+                .await
+                .unwrap(),
+            )
         };
         let event_registry = Arc::new(EventRegistry::new(Arc::downgrade(&model)));
         let event_source_factory = Arc::new(EventSourceFactory::new(
diff --git a/src/sys/component_manager/src/model/mod.rs b/src/sys/component_manager/src/model/mod.rs
index 96e3272..621ebba 100644
--- a/src/sys/component_manager/src/model/mod.rs
+++ b/src/sys/component_manager/src/model/mod.rs
@@ -14,6 +14,7 @@
 // fuctionality in this module. Factor out the externally-depended code into its own module.
 pub mod testing;
 
+pub(crate) mod component_id_index;
 pub(crate) mod context;
 pub(crate) mod environment;
 pub(crate) mod events;
diff --git a/src/sys/component_manager/src/model/model.rs b/src/sys/component_manager/src/model/model.rs
index d486974..fa19ecd 100644
--- a/src/sys/component_manager/src/model/model.rs
+++ b/src/sys/component_manager/src/model/model.rs
@@ -43,17 +43,21 @@
 
 impl Model {
     /// Creates a new component model and initializes its topology.
-    pub fn new(params: ModelParams) -> Model {
+    pub async fn new(params: ModelParams) -> Result<Model, ModelError> {
         let component_manager_realm =
             Arc::new(ComponentManagerRealm::new(params.namespace_capabilities));
-        let context = Arc::new(ModelContext::new(params.runtime_config));
+        let context = Arc::new(ModelContext::new(params.runtime_config).await?);
         let root_realm = Arc::new(Realm::new_root_realm(
             params.root_environment,
             Arc::downgrade(&context),
             Arc::downgrade(&component_manager_realm),
             params.root_component_url,
         ));
-        Model { root_realm, _context: context, _component_manager_realm: component_manager_realm }
+        Ok(Model {
+            root_realm,
+            _context: context,
+            _component_manager_realm: component_manager_realm,
+        })
     }
 
     /// Looks up a realm by absolute moniker. The component instance in the realm will be resolved
diff --git a/src/sys/lib/component_id_index/src/fidl_convert.rs b/src/sys/lib/component_id_index/src/fidl_convert.rs
index 6f7e42f..fbc0fea 100644
--- a/src/sys/lib/component_id_index/src/fidl_convert.rs
+++ b/src/sys/lib/component_id_index/src/fidl_convert.rs
@@ -7,7 +7,7 @@
 use std::convert::TryFrom;
 use thiserror::Error;
 
-#[derive(Error, Debug, PartialEq)]
+#[derive(Error, Clone, Debug, PartialEq)]
 pub enum FidlConversionError {
     #[error("Missing appmgr_restrict_isolated_persistent_storage")]
     MissingAppmgrRestrictIsolatedPersistentStorage,
diff --git a/src/sys/lib/component_id_index/src/lib.rs b/src/sys/lib/component_id_index/src/lib.rs
index 087ec00..6a8ea25 100644
--- a/src/sys/lib/component_id_index/src/lib.rs
+++ b/src/sys/lib/component_id_index/src/lib.rs
@@ -5,34 +5,44 @@
 // This library must remain platform-agnostic because it used by a host tool and within Fuchsia.
 
 use anyhow::{anyhow, Context, Result};
+use fidl_fuchsia_component_internal as fcomponent_internal;
 use serde::{Deserialize, Serialize};
 use std::collections::HashMap;
+use std::convert::TryFrom;
 use std::fs;
 use std::str;
 use thiserror::Error;
 
 pub mod fidl_convert;
 
-#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, 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)]
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
 pub struct InstanceIdEntry {
     pub instance_id: Option<String>,
     pub appmgr_moniker: Option<AppmgrMoniker>,
     pub moniker: Option<String>,
 }
 
-#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
 pub struct Index {
     pub appmgr_restrict_isolated_persistent_storage: Option<bool>,
     pub instances: Vec<InstanceIdEntry>,
 }
 
+#[derive(Debug, Clone, Error, PartialEq)]
+pub enum IndexError {
+    #[error("invalid index")]
+    ValidationError(#[from] ValidationError),
+    #[error("could not convert FIDL index")]
+    FidlConversionError(#[from] fidl_convert::FidlConversionError),
+}
+
 impl Index {
     // Construct an Index by merging index source files.
     //
@@ -42,22 +52,40 @@
     // See `ValidationError` for possible errors.
     pub fn from_files_with_decoder(
         index_file_paths: &[String],
-        decoder: impl Fn(&str) -> anyhow::Result<Index>,
+        decoder: impl Fn(&[u8]) -> 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())
+            let index = decoder(contents.as_str().as_bytes())
                 .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())
     }
+
+    // Construct an Index from the given FIDL-schema'd index.
+    //
+    // The given fidl_index is validated.
+    pub fn from_fidl(
+        fidl_index: fcomponent_internal::ComponentIdIndex,
+    ) -> Result<Index, IndexError> {
+        let native_index = Index::try_from(fidl_index)?;
+        let mut ctx = MergeContext::new();
+        ctx.merge("", &native_index)?;
+        Ok(ctx.output())
+    }
 }
 
-#[derive(Error, Debug, PartialEq)]
+impl Default for Index {
+    fn default() -> Self {
+        Index { appmgr_restrict_isolated_persistent_storage: Some(true), instances: vec![] }
+    }
+}
+
+#[derive(Error, Debug, Clone, 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 },
diff --git a/src/sys/lib/moniker/src/lib.rs b/src/sys/lib/moniker/src/lib.rs
index 25d5342..e380f65 100644
--- a/src/sys/lib/moniker/src/lib.rs
+++ b/src/sys/lib/moniker/src/lib.rs
@@ -535,7 +535,7 @@
 }
 
 /// Errors produced by `MonikerEnvironment`.
-#[derive(Debug, Error, PartialEq, Eq)]
+#[derive(Debug, Error, Clone, PartialEq, Eq)]
 pub enum MonikerError {
     #[error("invalid moniker: {}", rep)]
     InvalidMoniker { rep: String },
diff --git a/tools/component_id_index/src/main.rs b/tools/component_id_index/src/main.rs
index c981cc8..d2fec6c 100644
--- a/tools/component_id_index/src/main.rs
+++ b/tools/component_id_index/src/main.rs
@@ -41,7 +41,8 @@
 // Make an Index using a set of JSON5-encoded index files.
 fn merge_index_from_json5_files(index_files: &[String]) -> anyhow::Result<Index> {
     Index::from_files_with_decoder(index_files, |json5| {
-        serde_json5::from_str(json5).context("Unable to parse JSON5")
+        let json5_str = std::str::from_utf8(json5).context("Unable to parse as UTF-8")?;
+        serde_json5::from_str(json5_str).context("Unable to parse JSON5")
     }).map_err(|e|{
         match e.downcast_ref::<ValidationError>() {
             Some(ValidationError::MissingInstanceIds{entries}) => {
diff --git a/tools/component_manager_config/src/compile.rs b/tools/component_manager_config/src/compile.rs
index c0445e6..04dd1b4 100644
--- a/tools/component_manager_config/src/compile.rs
+++ b/tools/component_manager_config/src/compile.rs
@@ -32,6 +32,7 @@
     builtin_pkg_resolver: Option<BuiltinPkgResolver>,
     out_dir_contents: Option<OutDirContents>,
     root_component_url: Option<Url>,
+    component_id_index_path: Option<String>,
 }
 
 #[derive(Deserialize, Debug)]
@@ -186,6 +187,7 @@
                 Some(root_component_url) => Some(root_component_url.as_str().to_string()),
                 None => None,
             },
+            component_id_index_path: config.component_id_index_path,
             ..Self::EMPTY
         })
     }
@@ -274,6 +276,7 @@
         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);
         Ok(self)
     }
 
@@ -336,6 +339,7 @@
         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))?;
@@ -407,6 +411,7 @@
             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",
         }"#;
         let config = compile_str(input).expect("failed to compile");
         assert_eq!(
@@ -475,6 +480,7 @@
                 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()),
                 ..component_internal::Config::EMPTY
             }
         );