blob: 0a932ef5b8406fb6ccc67081678e6d1f6679b3a8 [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 anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::str;
use structopt::StructOpt;
use thiserror::Error;
#[derive(Debug, StructOpt)]
#[structopt(about = "Verify and merge component ID index files.")]
struct CommandLineOpts {
#[structopt(
short,
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_file"
)]
input_manifest: PathBuf,
#[structopt(short, long, help = "Where to write the merged index file.")]
output_file: PathBuf,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
struct AppmgrMoniker {
url: String,
realm_path: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
struct InstanceIdEntry {
instance_id: String,
appmgr_moniker: AppmgrMoniker,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct Index {
instances: Vec<InstanceIdEntry>,
}
impl Index {
fn from_file(path: &str) -> anyhow::Result<Index> {
let contents = fs::read_to_string(path).context("unable to read file")?;
json5::from_str::<Index>(&contents).context("unable to parse to json5")
}
}
// 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.
pub 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>,
}
#[derive(Error, Debug, PartialEq)]
enum MergeError {
#[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 },
}
impl MergeContext {
fn new() -> MergeContext {
MergeContext {
output_index: Index { 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<(), MergeError> {
for instance in &index.instances {
if let Some(previous_source_path) = self
.accumulated_instance_ids
.insert(instance.instance_id.clone(), source_index_path.to_string())
{
return Err(MergeError::DuplicateIds {
instance_id: instance.instance_id.clone(),
source1: previous_source_path.clone(),
source2: source_index_path.to_string(),
});
}
self.output_index.instances.push(instance.clone());
}
Ok(())
}
// Access the accumulated index from calls to merge().
fn output(self) -> Index {
self.output_index
}
}
fn run(input_manifest_path: PathBuf, output_index_path: PathBuf) -> anyhow::Result<()> {
let mut ctx = MergeContext::new();
let input_manifest =
fs::File::open(input_manifest_path).context("Could not open input manifest")?;
let input_files = BufReader::new(input_manifest).lines();
for input_file_path in input_files {
let path = input_file_path.context("Could not read line from input manifest")?;
let index = Index::from_file(&path)
.with_context(|| anyhow!("Could not parse index file {}", &path))?;
ctx.merge(&path, &index)
.with_context(|| anyhow!("Could not merge index file {}", &path))?;
}
let serialized_output =
json5::to_string(&ctx.output()).context("Could not json-encode merged index")?;
fs::write(output_index_path, serialized_output.as_bytes())
.context("Could not write merged index to file")
}
fn main() -> anyhow::Result<()> {
let opts = CommandLineOpts::from_args();
run(opts.input_manifest, opts.output_file)
}
#[cfg(test)]
mod tests {
use super::*;
use rand::*;
use std::io::Write;
use tempfile;
fn gen_instance_id() -> String {
// generate random 256bits into a byte array
let mut rng = rand::thread_rng();
let mut num: [u8; 256 / 8] = [0; 256 / 8];
rng.fill_bytes(&mut num);
// turn the byte array into a hex string
num.iter().map(|byte| format!("{:x}", byte)).collect::<Vec<String>>().join("")
}
fn gen_index(num_instances: u32) -> Index {
Index {
instances: (0..num_instances)
.map(|i| InstanceIdEntry {
instance_id: gen_instance_id(),
appmgr_moniker: 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()],
},
})
.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,
MergeError::DuplicateIds {
instance_id: index1.instances[0].instance_id.clone(),
source1: source1.to_string(),
source2: source2.to_string()
}
);
}
#[test]
fn index_from_json_file() {
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_manifest = 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(json5::to_string(&index1).unwrap().as_bytes()).unwrap();
// write the second index file
let index2 = gen_index(2);
tmp_input_index2.write_all(json5::to_string(&index2).unwrap().as_bytes()).unwrap();
assert!(matches!(
run(tmp_input_manifest.path().to_path_buf(), tmp_output_manifest.path().to_path_buf()),
Ok(_)
));
// assert that the output index file contains the merged index.
let mut ctx = MergeContext::new();
ctx.merge(&tmp_input_index1.path().to_str().unwrap(), &index1).unwrap();
ctx.merge(&tmp_input_index2.path().to_str().unwrap(), &index2).unwrap();
assert_eq!(
ctx.output(),
Index::from_file(&tmp_output_manifest.path().to_str().unwrap()).unwrap()
);
}
}