blob: a198c9326afec795da7ad7f534ff418342fde294 [file] [log] [blame]
// Copyright 2019 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 crate::errors::BuildError;
use crate::{CreationManifest, MetaContents, MetaPackage};
use fuchsia_merkle::{Hash, MerkleTree};
use std::collections::{btree_map, BTreeMap};
use std::{fs, io};
pub fn build(
creation_manifest: &CreationManifest,
meta_package: &MetaPackage,
meta_far_writer: impl io::Write,
) -> Result<(), BuildError> {
build_with_file_system(
creation_manifest,
meta_package,
meta_far_writer,
&ActualFileSystem {},
)
}
// Used to mock out native filesystem for testing
trait FileSystem<'a> {
type File: io::Read;
fn open(&'a self, path: &str) -> Result<Self::File, io::Error>;
fn len(&self, path: &str) -> Result<u64, io::Error>;
fn read(&self, path: &str) -> Result<Vec<u8>, io::Error>;
}
struct ActualFileSystem;
impl FileSystem<'_> for ActualFileSystem {
type File = std::fs::File;
fn open(&self, path: &str) -> Result<Self::File, io::Error> {
Ok(fs::File::open(path)?)
}
fn len(&self, path: &str) -> Result<u64, io::Error> {
Ok(fs::metadata(path)?.len())
}
fn read(&self, path: &str) -> Result<Vec<u8>, io::Error> {
fs::read(path)
}
}
fn build_with_file_system<'a>(
creation_manifest: &CreationManifest,
meta_package: &MetaPackage,
meta_far_writer: impl io::Write,
file_system: &'a impl FileSystem<'a>,
) -> Result<(), BuildError> {
let external_content_infos =
get_external_content_infos(creation_manifest.external_contents(), file_system)?;
let meta_contents = MetaContents::from_map(
external_content_infos.iter().map(|(path, info)| (path.clone(), info.hash)).collect(),
)?;
let mut meta_contents_bytes = Vec::new();
meta_contents.serialize(&mut meta_contents_bytes)?;
let mut meta_package_bytes = Vec::new();
meta_package.serialize(&mut meta_package_bytes)?;
let mut far_contents: BTreeMap<&str, Vec<u8>> = BTreeMap::new();
for (resource_path, source_path) in creation_manifest.far_contents() {
far_contents.insert(resource_path, file_system.read(source_path)?);
}
let insert_generated_file =
|resource_path: &'static str, content, far_contents: &mut BTreeMap<_, _>| match far_contents
.entry(resource_path)
{
btree_map::Entry::Vacant(entry) => {
entry.insert(content);
Ok(())
}
btree_map::Entry::Occupied(_) => Err(BuildError::ConflictingResource {
conflicting_resource_path: resource_path.to_string(),
}),
};
insert_generated_file("meta/contents", meta_contents_bytes, &mut far_contents)?;
insert_generated_file("meta/package", meta_package_bytes, &mut far_contents)?;
let mut meta_entries: BTreeMap<&str, (u64, Box<dyn io::Read>)> = BTreeMap::new();
for (resource_path, content) in &far_contents {
meta_entries.insert(resource_path, (content.len() as u64, Box::new(content.as_slice())));
}
fuchsia_archive::write(meta_far_writer, meta_entries)
.map_err(|e| BuildError::ArchiveWrite(e))?;
Ok(())
}
struct ExternalContentInfo<'a> {
_source_path: &'a str, // Field will be used when creating the PackageManifest
_size: u64, // Field will be used when creating the PackageManifest
hash: Hash,
}
fn get_external_content_infos<'a, 'b>(
external_contents: &'a BTreeMap<String, String>,
file_system: &'b impl FileSystem<'b>,
) -> Result<BTreeMap<String, ExternalContentInfo<'a>>, BuildError> {
external_contents
.iter()
.map(|(resource_path, _source_path)| -> Result<(String, ExternalContentInfo), BuildError> {
let file = file_system.open(_source_path)?;
Ok((
resource_path.clone(),
ExternalContentInfo {
_source_path,
_size: file_system.len(_source_path)?,
hash: MerkleTree::from_reader(file)?.root(),
},
))
})
.collect()
}
#[cfg(test)]
mod test_build_with_file_system {
use super::*;
use crate::test::*;
use maplit::{btreemap, hashmap};
use proptest::prelude::*;
use rand::SeedableRng;
use std::collections::{HashMap, HashSet};
use std::io;
use std::iter::FromIterator;
const GENERATED_FAR_CONTENTS: [&str; 2] = ["meta/contents", "meta/package"];
struct FakeFileSystem {
content_map: HashMap<String, Vec<u8>>,
}
impl FakeFileSystem {
fn from_creation_manifest_with_random_contents(
creation_manifest: &CreationManifest,
rng: &mut impl rand::Rng,
) -> FakeFileSystem {
let mut content_map = HashMap::new();
for (_, host_path) in
creation_manifest.far_contents().iter().chain(creation_manifest.external_contents())
{
let file_size = rng.gen_range(0, 6000);
content_map.insert(
host_path.to_string(),
rng.sample_iter(&rand::distributions::Standard).take(file_size).collect(),
);
}
Self { content_map }
}
}
impl<'a> FileSystem<'a> for FakeFileSystem {
type File = &'a [u8];
fn open(&'a self, path: &str) -> Result<Self::File, io::Error> {
Ok(self.content_map.get(path).unwrap().as_slice())
}
fn len(&self, path: &str) -> Result<u64, io::Error> {
Ok(self.content_map.get(path).unwrap().len() as u64)
}
fn read(&self, path: &str) -> Result<Vec<u8>, io::Error> {
Ok(self.content_map.get(path).unwrap().clone())
}
}
#[test]
fn test_verify_far_contents_with_fixed_inputs() {
let creation_manifest = CreationManifest::from_external_and_far_contents(
btreemap! {
"lib/mylib.so".to_string() => "host/mylib.so".to_string()
},
btreemap! {
"meta/my_component.cmx".to_string() => "host/my_component.cmx".to_string()
},
)
.unwrap();
let meta_package =
MetaPackage::from_name_and_variant("my-package-name", "my-package-variant").unwrap();
let mut meta_far_writer = Vec::new();
let component_manifest_contents = "my_component.cmx contents";
let file_system = FakeFileSystem {
content_map: hashmap! {
"host/mylib.so".to_string() => "mylib.so contents".as_bytes().to_vec(),
"host/my_component.cmx".to_string() => component_manifest_contents.as_bytes().to_vec()
},
};
build_with_file_system(
&creation_manifest,
&meta_package,
&mut meta_far_writer,
&file_system,
)
.unwrap();
let mut cursor = io::Cursor::new(meta_far_writer);
let mut reader = fuchsia_archive::Reader::new(&mut cursor).unwrap();
let actual_meta_package_bytes = reader.read_file("meta/package").unwrap();
let expected_meta_package_bytes =
br#"{"name":"my-package-name","version":"my-package-variant"}"#;
assert_eq!(actual_meta_package_bytes.as_slice(), &expected_meta_package_bytes[..]);
let actual_meta_contents_bytes = reader.read_file("meta/contents").unwrap();
let expected_meta_contents_bytes =
b"lib/mylib.so=4a886105646222c10428e5793868b13f536752d4b87e6497cdf9caed37e67410\n";
assert_eq!(actual_meta_contents_bytes.as_slice(), &expected_meta_contents_bytes[..]);
let actual_meta_component_bytes = reader.read_file("meta/my_component.cmx").unwrap();
assert_eq!(actual_meta_component_bytes.as_slice(), component_manifest_contents.as_bytes());
}
#[test]
fn test_reject_conflict_with_generated_file() {
for generated_file in &GENERATED_FAR_CONTENTS {
let creation_manifest = CreationManifest::from_external_and_far_contents(
BTreeMap::new(),
btreemap! {
generated_file.to_string() => "some-host-path".to_string()
},
)
.unwrap();
let meta_package =
MetaPackage::from_name_and_variant("my-package-name", "my-package-variant")
.unwrap();
let mut meta_far_writer = Vec::new();
let file_system = FakeFileSystem {
content_map: hashmap! {
"some-host-path".to_string() => Vec::new(),
},
};
let result = build_with_file_system(
&creation_manifest,
&meta_package,
&mut meta_far_writer,
&file_system,
);
assert_matches!(
result,
Err(BuildError::ConflictingResource {
conflicting_resource_path: path
}) => assert_eq!(path, generated_file.to_string()));
}
}
proptest! {
#[test]
fn test_meta_far_directory_names_are_exactly_generated_files_and_creation_manifest_far_contents(
creation_manifest in random_creation_manifest(),
meta_package in random_meta_package(),
seed in proptest::bits::u64::ANY)
{
let mut private_key_bytes = [0u8; 32];
let mut prng = rand::rngs::StdRng::seed_from_u64(seed);
prng.fill(&mut private_key_bytes);
let mut meta_far_writer = Vec::new();
let file_system = FakeFileSystem::from_creation_manifest_with_random_contents(
&creation_manifest, &mut prng);
build_with_file_system(
&creation_manifest,
&meta_package,
&mut meta_far_writer,
&file_system,
)
.unwrap();
let mut cursor = io::Cursor::new(meta_far_writer);
let reader = fuchsia_archive::Reader::new(&mut cursor).unwrap();
let expected_far_directory_names = {
let mut map: HashSet<&str> = HashSet::new();
for path in GENERATED_FAR_CONTENTS.iter() {
map.insert(*path);
}
for (path, _) in creation_manifest.far_contents().iter() {
map.insert(path);
}
map
};
let actual_far_directory_names: HashSet<&str> = reader.list().collect();
prop_assert_eq!(expected_far_directory_names, actual_far_directory_names);
}
#[test]
fn test_meta_far_contains_creation_manifest_far_contents(
creation_manifest in random_creation_manifest(),
meta_package in random_meta_package(),
seed in proptest::bits::u64::ANY)
{
let mut private_key_bytes = [0u8; 32];
let mut prng = rand::rngs::StdRng::seed_from_u64(seed);
prng.fill(&mut private_key_bytes);
let mut meta_far_writer = Vec::new();
let file_system = FakeFileSystem::from_creation_manifest_with_random_contents(
&creation_manifest, &mut prng);
build_with_file_system(
&creation_manifest,
&meta_package,
&mut meta_far_writer,
&file_system,
)
.unwrap();
let mut cursor = io::Cursor::new(meta_far_writer);
let mut reader = fuchsia_archive::Reader::new(&mut cursor).unwrap();
for (resource_path, host_path) in creation_manifest.far_contents().iter() {
let expected_contents = file_system.content_map.get(host_path).unwrap();
let actual_contents = reader.read_file(resource_path).unwrap();
prop_assert_eq!(expected_contents, &actual_contents);
}
}
#[test]
fn test_meta_far_meta_contents_lists_creation_manifest_external_contents(
creation_manifest in random_creation_manifest(),
meta_package in random_meta_package(),
seed in proptest::bits::u64::ANY)
{
let mut private_key_bytes = [0u8; 32];
let mut prng = rand::rngs::StdRng::seed_from_u64(seed);
prng.fill(&mut private_key_bytes);
let mut meta_far_writer: Vec<u8> = Vec::new();
let file_system = FakeFileSystem::from_creation_manifest_with_random_contents(
&creation_manifest, &mut prng);
build_with_file_system(
&creation_manifest,
&meta_package,
&mut meta_far_writer,
&file_system,
)
.unwrap();
let mut cursor = io::Cursor::new(meta_far_writer);
let mut reader = fuchsia_archive::Reader::new(&mut cursor).unwrap();
let meta_contents =
MetaContents::deserialize(
reader.read_file("meta/contents").unwrap().as_slice())
.unwrap();
let actual_external_contents: HashSet<&str> = meta_contents
.contents()
.keys()
.map(|s| s.as_str())
.collect();
let expected_external_contents: HashSet<&str> =
HashSet::from_iter(
creation_manifest
.external_contents()
.keys()
.map(|s| s.as_str()));
prop_assert_eq!(expected_external_contents, actual_external_contents);
}
}
}
#[cfg(test)]
mod test_build {
use super::*;
use crate::test::*;
use proptest::prelude::*;
use rand::SeedableRng;
use std::fs;
use std::io::{self, Write};
use tempfile::TempDir;
// Creates a temporary directory, then for each host path in the `CreationManifest`'s
// external contents and far contents maps creates a file in the temporary directory
// with path "${TEMP_DIR}/${HOST_PATH}" and random size and contents.
// Returns a new `CreationManifest` with updated host paths and the `TempDir`.
fn populate_filesystem_from_creation_manifest(
creation_manifest: CreationManifest,
rng: &mut impl rand::Rng,
) -> (CreationManifest, TempDir) {
let temp_dir = TempDir::new().unwrap();
let temp_dir_path = temp_dir.path();
fn populate_filesystem_and_make_new_map(
path_prefix: &std::path::Path,
resource_to_host_path: &BTreeMap<String, String>,
rng: &mut impl rand::Rng,
) -> BTreeMap<String, String> {
let mut new_map = BTreeMap::new();
for (resource_path, host_path) in resource_to_host_path {
let new_host_path = path_prefix.join(host_path).to_str().unwrap().to_string();
let mut f = fs::File::create(&new_host_path).unwrap();
let file_size = rng.gen_range(0, 6000);
f.write_all(
rng.sample_iter(&rand::distributions::Standard)
.take(file_size)
.collect::<Vec<u8>>()
.as_slice(),
)
.unwrap();
new_map.insert(resource_path.to_string(), new_host_path);
}
new_map
}
let new_far_contents = populate_filesystem_and_make_new_map(
temp_dir_path,
creation_manifest.far_contents(),
rng,
);
let new_external_contents = populate_filesystem_and_make_new_map(
temp_dir_path,
creation_manifest.external_contents(),
rng,
);
let new_creation_manifest = CreationManifest::from_external_and_far_contents(
new_external_contents,
new_far_contents,
)
.unwrap();
(new_creation_manifest, temp_dir)
}
proptest! {
#[test]
fn test_meta_far_contains_creation_manifest_far_contents(
creation_manifest in random_creation_manifest(),
meta_package in random_meta_package(),
seed in proptest::bits::u64::ANY)
{
let mut prng = rand::rngs::StdRng::seed_from_u64(seed);
let (creation_manifest, _temp_dir) = populate_filesystem_from_creation_manifest(creation_manifest, &mut prng);
let mut private_key_bytes = [0u8; 32];
prng.fill(&mut private_key_bytes);
let mut meta_far_writer = Vec::new();
build(
&creation_manifest,
&meta_package,
&mut meta_far_writer,
)
.unwrap();
let mut cursor = io::Cursor::new(meta_far_writer);
let mut reader = fuchsia_archive::Reader::new(&mut cursor).unwrap();
for (resource_path, host_path) in creation_manifest.far_contents().iter() {
let expected_contents = std::fs::read(host_path).unwrap();
let actual_contents = reader.read_file(resource_path).unwrap();
prop_assert_eq!(expected_contents, actual_contents);
}
}
}
}