blob: 0e04713820e204f86e9962c0023ef068e3ca017d [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::CreationManifestError;
use serde_derive::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::io;
/// A `CreationManifest` lists the files that should be included in a Fuchsia package.
/// Both `external_contents` and `far_contents` are maps from package resource paths in
/// the to-be-created package to paths on the local filesystem.
/// Package resource paths start with "meta/" if and only if they are in `far_contents`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CreationManifest {
external_contents: BTreeMap<String, String>,
far_contents: BTreeMap<String, String>,
}
impl CreationManifest {
/// `external_contents` lists the blobs that make up the meta/contents file
pub fn external_contents(&self) -> &BTreeMap<String, String> {
&self.external_contents
}
/// `far_contents` lists the files to be included bodily in the meta.far
pub fn far_contents(&self) -> &BTreeMap<String, String> {
&self.far_contents
}
/// Creates a `CreationManifest` from external and far contents maps.
///
/// `external_contents` is a map from package resource paths to their locations
/// on the host filesystem. These are the files that will be listed in
/// `meta/contents`. Resource paths in `external_contents` must *not* start with
/// `meta/`.
/// `far_contents` is a map from package resource paths to their locations
/// on the host filesystem. These are the files that will be included bodily in the
/// package `meta.far` archive. Resource paths in `far_contents` must start with
/// `meta/`.
///
/// # Examples
///
/// ```
/// # use fuchsia_pkg::CreationManifest;
/// # use maplit::btreemap;
/// let external_contents = btreemap! {
/// "lib/mylib.so".to_string() => "build/system/path/mylib.so".to_string()
/// };
/// let far_contents = btreemap! {
/// "meta/my_component_manifest.cmx".to_string() =>
/// "other/build/system/path/my_component_manifest.cmx".to_string()
/// };
/// let creation_manifest =
/// CreationManifest::from_external_and_far_contents(external_contents, far_contents)
/// .unwrap();
/// ```
pub fn from_external_and_far_contents(
external_contents: BTreeMap<String, String>,
far_contents: BTreeMap<String, String>,
) -> Result<Self, CreationManifestError> {
for (resource_path, _) in external_contents.iter().chain(far_contents.iter()) {
crate::path::check_resource_path(&resource_path).map_err(|e| {
CreationManifestError::ResourcePath { cause: e, path: resource_path.to_string() }
})?;
}
for (resource_path, _) in external_contents.iter() {
if resource_path.starts_with("meta/") {
return Err(CreationManifestError::ExternalContentInMetaDirectory {
path: resource_path.to_string(),
});
}
}
for (resource_path, _) in far_contents.iter() {
if !resource_path.starts_with("meta/") {
return Err(CreationManifestError::FarContentNotInMetaDirectory {
path: resource_path.to_string(),
});
}
}
Ok(CreationManifest { external_contents, far_contents })
}
/// Deserializes a `CreationManifest` from versioned json.
///
/// # Examples
/// ```
/// # use fuchsia_pkg::CreationManifest;
/// let json_string = r#"
/// {
/// "version": "1",
/// "content": {
/// "/": {
/// "lib/mylib.so": "build/system/path/mylib.so"
/// },
/// "/meta/": {
/// "my_component_manifest.cmx": "other/build/system/path/my_component_manifest.cmx"
/// }
/// }"#;
/// let creation_manifest = CreationManifest::from_json(json_string.as_bytes());
/// ```
pub fn from_json<R: io::Read>(reader: R) -> Result<Self, CreationManifestError> {
match serde_json::from_reader::<R, VersionedCreationManifest>(reader)? {
VersionedCreationManifest::Version1(v1) => CreationManifest::from_v1(v1),
}
}
fn from_v1(v1: CreationManifestV1) -> Result<Self, CreationManifestError> {
let mut far_contents = BTreeMap::new();
// Validate package resource paths in far contents before "meta/" is prepended
// for better error messages.
for (resource_path, host_path) in v1.far_contents.into_iter() {
crate::path::check_resource_path(&resource_path).map_err(|e| {
CreationManifestError::ResourcePath { cause: e, path: resource_path.to_string() }
})?;
far_contents.insert(format!("meta/{}", resource_path), host_path);
}
CreationManifest::from_external_and_far_contents(v1.external_contents, far_contents)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(tag = "version", content = "content", deny_unknown_fields)]
enum VersionedCreationManifest {
#[serde(rename = "1")]
Version1(CreationManifestV1),
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
struct CreationManifestV1 {
#[serde(rename = "/")]
external_contents: BTreeMap<String, String>,
#[serde(rename = "/meta/")]
far_contents: BTreeMap<String, String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::errors::ResourcePathError::PathStartsWithSlash;
use crate::test::*;
use maplit::btreemap;
use proptest::prelude::*;
use serde_json::json;
fn from_json_value(
value: serde_json::Value,
) -> Result<CreationManifest, CreationManifestError> {
CreationManifest::from_json(value.to_string().as_bytes())
}
#[test]
fn test_malformed_json() {
assert_matches!(
CreationManifest::from_json("<invalid json document>".as_bytes()),
Err(CreationManifestError::Json(err)) => assert!(err.is_syntax())
);
}
#[test]
fn test_invalid_version() {
assert_matches!(
from_json_value(json!({"version": "2", "content": {}})),
Err(CreationManifestError::Json(err)) => assert!(err.is_data()));
}
#[test]
fn test_invalid_resource_path() {
assert_matches!(
from_json_value(
json!(
{"version": "1",
"content":
{"/meta/" :
{"/starts-with-slash": "host-path"},
"/": {
}
}
}
)
),
Err(CreationManifestError::ResourcePath {
cause: PathStartsWithSlash,
path: s
})
=> assert_eq!(s, "/starts-with-slash"));
}
#[test]
fn test_meta_in_external() {
assert_matches!(
from_json_value(
json!(
{"version": "1",
"content":
{"/meta/" : {},
"/": {
"meta/foo": "host-path"}
}
}
)
),
Err(CreationManifestError::ExternalContentInMetaDirectory{path: s})
=> assert_eq!(s, "meta/foo"));
}
#[test]
fn test_from_v1() {
assert_eq!(
from_json_value(json!(
{"version": "1",
"content": {
"/": {
"this-path": "this-host-path",
"that/path": "that/host/path"},
"/meta/" : {
"some-path": "some-host-path",
"other/path": "other/host/path"}
}
}
))
.unwrap(),
CreationManifest {
external_contents: btreemap! {
"this-path".to_string() => "this-host-path".to_string(),
"that/path".to_string() => "that/host/path".to_string()
},
far_contents: btreemap! {
"meta/some-path".to_string() => "some-host-path".to_string(),
"meta/other/path".to_string() => "other/host/path".to_string()
}
}
);
}
proptest! {
#[test]
fn test_from_external_and_far_contents_does_not_modify_valid_maps(
ref external_resource_path in random_resource_path(1, 3),
ref external_host_path in ".{0,30}",
ref far_resource_path in random_resource_path(1, 3),
ref far_host_path in ".{0,30}"
) {
prop_assume!(!external_resource_path.starts_with("meta/"));
let external_contents = btreemap! {
external_resource_path.to_string() => external_host_path.to_string()
};
let far_resource_path = format!("meta/{}", far_resource_path);
let far_contents = btreemap! {
far_resource_path.to_string() => far_host_path.to_string()
};
let creation_manifest = CreationManifest::from_external_and_far_contents(
external_contents.clone(), far_contents.clone())
.unwrap();
prop_assert_eq!(creation_manifest.external_contents(), &external_contents);
prop_assert_eq!(creation_manifest.far_contents(), &far_contents);
}
}
}