| // 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 anyhow::{anyhow, Context, Result}; | 
 |  | 
 | use component_id_index::*; | 
 | use fidl::encoding::encode_persistent; | 
 | use fidl_fuchsia_component_internal as fcomponent_internal; | 
 | use serde_json; | 
 | use serde_json5; | 
 | use std::convert::TryInto; | 
 | use std::fs; | 
 | use std::io::{BufRead, BufReader}; | 
 | use std::path::PathBuf; | 
 | use structopt::StructOpt; | 
 |  | 
 | #[derive(Debug, StructOpt)] | 
 | #[structopt(about = "Validate and merge component ID index files.")] | 
 | struct CommandLineOpts { | 
 |     #[structopt( | 
 |         long, | 
 |         help = "Path to a manifest text file containing a list of index files, one on each line. All index files are merged into a single index, written to the supplied --output_index_json and --output_index_fidl" | 
 |     )] | 
 |     input_manifest: PathBuf, | 
 |  | 
 |     #[structopt(long, help = "Where to write the merged index file, encoded in JSON.")] | 
 |     output_index_json: PathBuf, | 
 |  | 
 |     #[structopt(long, help = "Where to write the merged index file, encoded in FIDL wire-format.")] | 
 |     output_index_fidl: PathBuf, | 
 |  | 
 |     #[structopt( | 
 |         short, | 
 |         long, | 
 |         help = "Where to write a dep file (.d) file of indices from --input_manifest." | 
 |     )] | 
 |     depfile: PathBuf, | 
 | } | 
 |  | 
 | // 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| { | 
 |         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}) => { | 
 |                 let corrected_entries = generate_instance_ids(&entries); | 
 |                 anyhow!("Some entries are missing `instance_id` fields. Here are some generated IDs for you:\n{}\n\nSee https://fuchsia.dev/fuchsia-src/development/components/component_id_index#defining_an_index for more details.", | 
 |                             serde_json::to_string_pretty(&corrected_entries).unwrap()) | 
 |             }, | 
 |             _ => e | 
 |         } | 
 |     }) | 
 | } | 
 |  | 
 | fn generate_instance_ids(entries: &Vec<InstanceIdEntry>) -> Vec<InstanceIdEntry> { | 
 |     let rng = &mut rand::thread_rng(); | 
 |     (0..entries.len()) | 
 |         .map(|i| { | 
 |             let mut with_id = entries[i].clone(); | 
 |             with_id.instance_id = Some(gen_instance_id(rng)); | 
 |             with_id | 
 |         }) | 
 |         .collect::<Vec<InstanceIdEntry>>() | 
 | } | 
 |  | 
 | fn run(opts: CommandLineOpts) -> anyhow::Result<()> { | 
 |     let input_manifest = | 
 |         fs::File::open(opts.input_manifest).context("Could not open input manifest")?; | 
 |     let input_files = BufReader::new(input_manifest) | 
 |         .lines() | 
 |         .collect::<Result<Vec<String>, _>>() | 
 |         .context("Could not read input manifest")?; | 
 |     let merged_index = merge_index_from_json5_files(input_files.as_slice())?; | 
 |  | 
 |     let serialized_output_json = | 
 |         serde_json::to_string(&merged_index).context("Could not json-encode merged index")?; | 
 |     fs::write(&opts.output_index_json, serialized_output_json.as_bytes()) | 
 |         .context("Could not write merged JSON-encoded index to file")?; | 
 |  | 
 |     let mut merged_index_fidl: fcomponent_internal::ComponentIdIndex = merged_index.try_into()?; | 
 |     let serialized_output_fidl = | 
 |         encode_persistent(&mut merged_index_fidl).context("Could not fidl-encode merged index")?; | 
 |     fs::write(&opts.output_index_fidl, serialized_output_fidl) | 
 |         .context("Could not write merged FIDL-encoded index to file")?; | 
 |  | 
 |     // write out the depfile | 
 |     fs::write( | 
 |         &opts.depfile, | 
 |         format!( | 
 |             "{}: {}\n{}: {}\n", | 
 |             opts.output_index_json.to_str().unwrap(), | 
 |             input_files.join(" "), | 
 |             opts.output_index_fidl.to_str().unwrap(), | 
 |             input_files.join(" ") | 
 |         ), | 
 |     ) | 
 |     .context("Could not write to depfile") | 
 | } | 
 |  | 
 | fn main() -> anyhow::Result<()> { | 
 |     let opts = CommandLineOpts::from_args(); | 
 |     run(opts) | 
 | } | 
 |  | 
 | #[cfg(test)] | 
 | mod tests { | 
 |     use super::*; | 
 |     use moniker::AbsoluteMoniker; | 
 |     use pretty_assertions::assert_eq; | 
 |     use regex; | 
 |     use std::io::Write; | 
 |     use tempfile; | 
 |  | 
 |     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: Some( | 
 |                         AbsoluteMoniker::parse_string_without_instances("/a/b/c").unwrap(), | 
 |                     ), | 
 |                 }) | 
 |                 .collect(), | 
 |         } | 
 |     } | 
 |  | 
 |     fn make_index_file(index: &Index) -> anyhow::Result<tempfile::NamedTempFile> { | 
 |         let mut index_file = tempfile::NamedTempFile::new()?; | 
 |         index_file.write_all(serde_json::to_string(index).unwrap().as_bytes())?; | 
 |         Ok(index_file) | 
 |     } | 
 |  | 
 |     #[test] | 
 |     fn error_missing_instance_ids() { | 
 |         let mut index = gen_index(4); | 
 |         index.instances[1].instance_id = None; | 
 |         index.instances[3].instance_id = None; | 
 |  | 
 |         let index_file = make_index_file(&index).unwrap(); | 
 |         let index_files = [String::from(index_file.path().to_str().unwrap())]; | 
 |  | 
 |         // this should be an error, since `index` has entries with a missing instance ID. | 
 |         // check the error output's message as well. | 
 |         let merge_result = merge_index_from_json5_files(&index_files); | 
 |         let actual_output = merge_result.err().unwrap().to_string(); | 
 |         let expected_output = r#"Some entries are missing `instance_id` fields. Here are some generated IDs for you: | 
 | [ | 
 |   { | 
 |     "instance_id": "RANDOM_GENERATED_INSTANCE_ID", | 
 |     "appmgr_moniker": { | 
 |       "url": "fuchsia-pkg://example.com/fake_pkg#meta/fake_component_1.cmx", | 
 |       "realm_path": [ | 
 |         "root", | 
 |         "child", | 
 |         "1" | 
 |       ], | 
 |       "transitional_realm_paths": null | 
 |     }, | 
 |     "moniker": "/a/b/c" | 
 |   }, | 
 |   { | 
 |     "instance_id": "RANDOM_GENERATED_INSTANCE_ID", | 
 |     "appmgr_moniker": { | 
 |       "url": "fuchsia-pkg://example.com/fake_pkg#meta/fake_component_3.cmx", | 
 |       "realm_path": [ | 
 |         "root", | 
 |         "child", | 
 |         "3" | 
 |       ], | 
 |       "transitional_realm_paths": null | 
 |     }, | 
 |     "moniker": "/a/b/c" | 
 |   } | 
 | ] | 
 |  | 
 | See https://fuchsia.dev/fuchsia-src/development/components/component_id_index#defining_an_index for more details."#; | 
 |  | 
 |         let re = regex::Regex::new("[0-9a-f]{64}").unwrap(); | 
 |         let actual_output_modified = re.replace_all(&actual_output, "RANDOM_GENERATED_INSTANCE_ID"); | 
 |         assert_eq!(actual_output_modified, expected_output); | 
 |     } | 
 |  | 
 |     #[test] | 
 |     fn index_from_json5() { | 
 |         let mut index_file = tempfile::NamedTempFile::new().unwrap(); | 
 |         index_file | 
 |             .write_all( | 
 |                 r#"{ | 
 |             // Here is a comment. | 
 |             instances: [] | 
 |         }"# | 
 |                 .as_bytes(), | 
 |             ) | 
 |             .unwrap(); | 
 |  | 
 |         // only checking that we parsed successfully. | 
 |         let files = [String::from(index_file.path().to_str().unwrap())]; | 
 |         assert!(merge_index_from_json5_files(&files).is_ok()); | 
 |     } | 
 |  | 
 |     #[test] | 
 |     fn multiple_indices_in_manifest() { | 
 |         let mut tmp_input_manifest = tempfile::NamedTempFile::new().unwrap(); | 
 |         let mut tmp_input_index1 = tempfile::NamedTempFile::new().unwrap(); | 
 |         let mut tmp_input_index2 = tempfile::NamedTempFile::new().unwrap(); | 
 |         let tmp_output_index_json = tempfile::NamedTempFile::new().unwrap(); | 
 |         let tmp_output_index_fidl = tempfile::NamedTempFile::new().unwrap(); | 
 |         let tmp_output_depfile = tempfile::NamedTempFile::new().unwrap(); | 
 |  | 
 |         // the manifest lists two index files: | 
 |         write!( | 
 |             tmp_input_manifest, | 
 |             "{}\n{}", | 
 |             tmp_input_index1.path().display(), | 
 |             tmp_input_index2.path().display() | 
 |         ) | 
 |         .unwrap(); | 
 |  | 
 |         // write the first index file | 
 |         let index1 = gen_index(2); | 
 |         tmp_input_index1.write_all(serde_json5::to_string(&index1).unwrap().as_bytes()).unwrap(); | 
 |  | 
 |         // write the second index file | 
 |         let index2 = gen_index(2); | 
 |         tmp_input_index2.write_all(serde_json5::to_string(&index2).unwrap().as_bytes()).unwrap(); | 
 |  | 
 |         assert!(matches!( | 
 |             run(CommandLineOpts { | 
 |                 input_manifest: tmp_input_manifest.path().to_path_buf(), | 
 |                 output_index_json: tmp_output_index_json.path().to_path_buf(), | 
 |                 output_index_fidl: tmp_output_index_fidl.path().to_path_buf(), | 
 |                 depfile: tmp_output_depfile.path().to_path_buf(), | 
 |             }), | 
 |             Ok(_) | 
 |         )); | 
 |  | 
 |         // assert that the output index file contains the merged index. | 
 |         let mut merged_index = index1.clone(); | 
 |         merged_index.instances.extend_from_slice(&index2.instances); | 
 |         let index_files = [String::from(tmp_output_index_json.path().to_str().unwrap())]; | 
 |         assert_eq!(merged_index, merge_index_from_json5_files(&index_files).unwrap()); | 
 |  | 
 |         // assert the structure of the dependency file: | 
 |         //  <merged_output_index>: <input index 1> <input index 2>\n | 
 |         assert_eq!( | 
 |             format!( | 
 |                 "{}: {} {}\n{}: {} {}\n", | 
 |                 tmp_output_index_json.path().to_str().unwrap(), | 
 |                 tmp_input_index1.path().to_str().unwrap(), | 
 |                 tmp_input_index2.path().to_str().unwrap(), | 
 |                 tmp_output_index_fidl.path().to_str().unwrap(), | 
 |                 tmp_input_index1.path().to_str().unwrap(), | 
 |                 tmp_input_index2.path().to_str().unwrap() | 
 |             ), | 
 |             fs::read_to_string(tmp_output_depfile.path()).unwrap() | 
 |         ) | 
 |     } | 
 | } |