blob: d06bd208acdae73b9abfbc20a9637c93a96823c4 [file] [log] [blame]
// Copyright 2021 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 fuchsia_pkg::PackageManifest;
use serde::de::{self, Deserializer};
use serde::ser::Serializer;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
/// A manifest containing a list of images produced by the Image Assembler.
///
/// ```
/// use images_manifest::ImagesManifest;
///
/// let manifest = ImagesManifest {
/// images: vec![
/// Image::ZBI {
/// path: "path/to/fuchsia.zbi",
/// signed: false,
/// },
/// Image::VBMeta("path/to/fuchsia.vbmeta"),
/// Image::FVM("path/to/fvm.blk"),
/// Image::FVMSparse("path/to/fvm.sparse.blk"),
/// ],
/// };
/// println!("{:?}", serde_json::to_value(manifest).unwrap());
/// ```
///
#[derive(Deserialize, Serialize, Debug, Default)]
#[serde(transparent)]
pub struct ImagesManifest {
/// List of images in the manifest.
pub images: Vec<Image>,
}
/// A specific Image type.
#[derive(Debug)]
pub enum Image {
/// Base Package.
BasePackage(PathBuf),
/// Zircon Boot Image.
ZBI {
/// Path to the ZBI image.
path: PathBuf,
/// Whether the ZBI is signed.
signed: bool,
},
/// Verified Boot Metadata.
VBMeta(PathBuf),
/// BlobFS image.
BlobFS {
/// Path to the BlobFS image.
path: PathBuf,
/// Contents metadata.
contents: BlobfsContents,
},
/// Fuchsia Volume Manager.
FVM(PathBuf),
/// Sparse FVM.
FVMSparse(PathBuf),
/// Sparse blobfs-only FVM.
FVMSparseBlob(PathBuf),
/// Fastboot FVM.
FVMFastboot(PathBuf),
/// Qemu Kernel.
QemuKernel(PathBuf),
}
impl Image {
/// Get the path of the image on the host.
pub fn source(&self) -> &PathBuf {
match self {
Image::BasePackage(s) => s,
Image::ZBI { path, signed: _ } => path,
Image::VBMeta(s) => s,
Image::BlobFS { path, .. } => path,
Image::FVM(s) => s,
Image::FVMSparse(s) => s,
Image::FVMSparseBlob(s) => s,
Image::FVMFastboot(s) => s,
Image::QemuKernel(s) => s,
}
}
}
#[derive(Debug, Serialize)]
struct ImageSerializeHelper<'a> {
#[serde(rename = "type")]
partition_type: &'a str,
name: &'a str,
path: &'a Path,
#[serde(skip_serializing_if = "Option::is_none")]
signed: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
contents: Option<ImageContentsSerializeHelper<'a>>,
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
enum ImageContentsSerializeHelper<'a> {
Blobfs(&'a BlobfsContents),
}
impl Serialize for Image {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let helper = match self {
Image::BasePackage(path) => ImageSerializeHelper {
partition_type: "far",
name: "base-package",
path,
signed: None,
contents: None,
},
Image::ZBI { path, signed } => ImageSerializeHelper {
partition_type: "zbi",
name: "zircon-a",
path,
signed: Some(*signed),
contents: None,
},
Image::VBMeta(path) => ImageSerializeHelper {
partition_type: "vbmeta",
name: "zircon-a",
path,
signed: None,
contents: None,
},
Image::BlobFS { path, contents } => ImageSerializeHelper {
partition_type: "blk",
name: "blob",
path,
signed: None,
contents: Some(ImageContentsSerializeHelper::Blobfs(contents)),
},
Image::FVM(path) => ImageSerializeHelper {
partition_type: "blk",
name: "storage-full",
path,
signed: None,
contents: None,
},
Image::FVMSparse(path) => ImageSerializeHelper {
partition_type: "blk",
name: "storage-sparse",
path,
signed: None,
contents: None,
},
Image::FVMSparseBlob(path) => ImageSerializeHelper {
partition_type: "blk",
name: "storage-sparse-blob",
path,
signed: None,
contents: None,
},
Image::FVMFastboot(path) => ImageSerializeHelper {
partition_type: "blk",
name: "fvm.fastboot",
path,
signed: None,
contents: None,
},
Image::QemuKernel(path) => ImageSerializeHelper {
partition_type: "kernel",
name: "qemu-kernel",
path,
signed: None,
contents: None,
},
};
helper.serialize(serializer)
}
}
/// Detailed metadata on the contents of a particular image output.
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct BlobfsContents {
/// Information about packages included in the image.
pub packages: PackagesMetadata,
/// Maximum total size of all the blobs stored in this image.
pub maximum_contents_size: Option<u64>,
/// List of blobs across all packages
pub blobs: PackageSetBlobInfo,
}
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(transparent)]
pub struct PackageSetBlobInfo(BTreeSet<PackageBlob>);
impl BlobfsContents {
/// Add base package info into BlobfsContents
pub fn add_base_package(&mut self, path: impl AsRef<Path>) -> anyhow::Result<()> {
Self::add_package(&mut self.packages.base, &mut self.blobs, path)?;
Ok(())
}
/// Add cache package info into BlobfsContents
pub fn add_cache_package(&mut self, path: impl AsRef<Path>) -> anyhow::Result<()> {
Self::add_package(&mut self.packages.cache, &mut self.blobs, path)?;
Ok(())
}
fn add_package(
package_set: &mut PackageSetMetadata,
content_blobs: &mut PackageSetBlobInfo,
path: impl AsRef<Path>,
) -> anyhow::Result<()> {
let manifest = path.as_ref().to_owned();
let package_manifest = PackageManifest::try_load_from(&manifest)?;
let name = package_manifest.name().to_string();
let mut package_blobs: Vec<PackageBlob> = vec![];
for blob in package_manifest.into_blobs() {
content_blobs.0.insert(PackageBlob {
merkle: blob.merkle.to_string(),
path: blob.path.to_string(),
used_space_in_blobfs: 0, /* Use 0 until we populate with compressed size */
});
package_blobs.push(PackageBlob {
merkle: blob.merkle.to_string(),
path: blob.path.to_string(),
used_space_in_blobfs: 0, /* Use 0 until we populate with compressed size */
});
}
package_blobs.sort();
package_set.0.push(PackageMetadata { name, manifest, blobs: package_blobs });
Ok(())
}
}
/// Metadata on packages included in a given image.
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct PackagesMetadata {
/// Paths to package manifests for the base package set.
pub base: PackageSetMetadata,
/// Paths to package manifests for the cache package set.
pub cache: PackageSetMetadata,
}
/// Metadata for a certain package set (e.g. base or cache).
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(transparent)]
pub struct PackageSetMetadata(pub Vec<PackageMetadata>);
/// Metadata on a single package included in a given image.
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct PackageMetadata {
/// The package's name.
pub name: String,
/// Path to the package's manifest.
pub manifest: PathBuf,
/// List of blobs in this package.
pub blobs: Vec<PackageBlob>,
}
#[derive(Clone, Debug, Ord, PartialOrd, PartialEq, Eq, Deserialize, Serialize)]
pub struct PackageBlob {
// Merkle hash of this blob
pub merkle: String,
// Path of blob in package
pub path: String,
// Space used by this blob in blobfs
pub used_space_in_blobfs: u64,
}
#[derive(Debug, Deserialize)]
struct ImageDeserializeHelper {
#[serde(rename = "type")]
partition_type: String,
name: String,
path: PathBuf,
signed: Option<bool>,
contents: Option<ImageContentsDeserializeHelper>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum ImageContentsDeserializeHelper {
Blobfs(BlobfsContents),
}
impl<'de> Deserialize<'de> for Image {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let helper = ImageDeserializeHelper::deserialize(deserializer)?;
match (&helper.partition_type[..], &helper.name[..], &helper.signed) {
("far", "base-package", None) => Ok(Image::BasePackage(helper.path)),
("zbi", "zircon-a", Some(signed)) => {
Ok(Image::ZBI { path: helper.path, signed: *signed })
}
("vbmeta", "zircon-a", None) => Ok(Image::VBMeta(helper.path)),
("blk", "blob", None) => {
if let Some(contents) = helper.contents {
let ImageContentsDeserializeHelper::Blobfs(contents) = contents;
Ok(Image::BlobFS { path: helper.path, contents })
} else {
Err(de::Error::missing_field("contents"))
}
}
("blk", "storage-full", None) => Ok(Image::FVM(helper.path)),
("blk", "storage-sparse", None) => Ok(Image::FVMSparse(helper.path)),
("blk", "storage-sparse-blob", None) => Ok(Image::FVMSparseBlob(helper.path)),
("blk", "fvm.fastboot", None) => Ok(Image::FVMFastboot(helper.path)),
("kernel", "qemu-kernel", None) => Ok(Image::QemuKernel(helper.path)),
(partition_type, name, _) => Err(de::Error::unknown_variant(
&format!("({}, {})", partition_type, name),
&[
"(far, base-package)",
"(zbi, zircon-a)",
"(vbmeta, zircon-a)",
"(blk, blob)",
"(blk, storage-full)",
"(blk, storage-sparse)",
"(blk, storage-sparse-blob)",
"(blk, fvm.fastboot)",
"(kernel, qemu-kernel)",
],
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::{json, Value};
use std::io::Write;
use tempfile::{tempdir, NamedTempFile};
#[test]
fn serialize() {
let manifest = ImagesManifest {
images: vec![
Image::BasePackage("path/to/base.far".into()),
Image::ZBI { path: "path/to/fuchsia.zbi".into(), signed: true },
Image::VBMeta("path/to/fuchsia.vbmeta".into()),
Image::BlobFS { path: "path/to/blob.blk".into(), contents: Default::default() },
Image::FVM("path/to/fvm.blk".into()),
Image::FVMSparse("path/to/fvm.sparse.blk".into()),
Image::FVMSparseBlob("path/to/fvm.blob.sparse.blk".into()),
Image::FVMFastboot("path/to/fvm.fastboot.blk".into()),
Image::QemuKernel("path/to/qemu/kernel".into()),
],
};
assert_eq!(generate_test_value(), serde_json::to_value(manifest).unwrap());
}
#[test]
fn serialize_unsigned_zbi() {
let manifest = ImagesManifest {
images: vec![Image::ZBI { path: "path/to/fuchsia.zbi".into(), signed: false }],
};
let value = json!([
{
"type": "zbi",
"name": "zircon-a",
"path": "path/to/fuchsia.zbi",
"signed": false,
}
]);
assert_eq!(value, serde_json::to_value(manifest).unwrap());
}
#[test]
fn deserialize_zbi_missing_signed() {
let invalid = json!([
{
"type": "zbi",
"name": "zircon-a",
"path": "path/to/fuchsia.zbi",
}
]);
let result: Result<ImagesManifest, _> = serde_json::from_value(invalid);
assert!(result.unwrap_err().is_data());
}
#[test]
fn deserialize() {
let manifest: ImagesManifest = serde_json::from_value(generate_test_value()).unwrap();
assert_eq!(manifest.images.len(), 9);
for image in &manifest.images {
let (expected, actual) = match image {
Image::BasePackage(path) => ("path/to/base.far", path),
Image::ZBI { path, signed } => {
assert!(signed);
("path/to/fuchsia.zbi", path)
}
Image::VBMeta(path) => ("path/to/fuchsia.vbmeta", path),
Image::BlobFS { path, contents } => {
assert_eq!(contents, &BlobfsContents::default());
("path/to/blob.blk", path)
}
Image::FVM(path) => ("path/to/fvm.blk", path),
Image::FVMSparse(path) => ("path/to/fvm.sparse.blk", path),
Image::FVMSparseBlob(path) => ("path/to/fvm.blob.sparse.blk", path),
Image::FVMFastboot(path) => ("path/to/fvm.fastboot.blk", path),
Image::QemuKernel(path) => ("path/to/qemu/kernel", path),
};
assert_eq!(&PathBuf::from(expected), actual);
}
}
#[test]
fn deserialize_invalid() {
let invalid = json!([
{
"type": "far-invalid",
"name": "base-package",
"path": "path/to/base.far",
},
]);
let result: Result<ImagesManifest, _> = serde_json::from_value(invalid);
assert!(result.unwrap_err().is_data());
}
#[test]
fn test_blobfs_contents_add_base_or_cache_package() -> anyhow::Result<()> {
let content = generate_test_package_manifest_content();
let mut package_manifest_temp_file = NamedTempFile::new()?;
let dir = tempdir().unwrap();
write!(package_manifest_temp_file, "{}", content)?;
let path = package_manifest_temp_file.into_temp_path();
path.persist(dir.path().join("package_manifest_temp_file.json"))?;
let mut contents = BlobfsContents::default();
contents.add_base_package(dir.path().join("package_manifest_temp_file.json"))?;
contents.add_cache_package(dir.path().join("package_manifest_temp_file.json"))?;
let actual_package_blobs_base = &(contents.packages.base.0[0].blobs);
let actual_package_blobs_cache = &(contents.packages.cache.0[0].blobs);
let package_blob1 = PackageBlob {
merkle: "7ddff816740d5803358dd4478d8437585e8d5c984b4361817d891807a16ff581".to_string(),
path: "bin/def".to_string(),
used_space_in_blobfs: 0,
};
let package_blob2 = PackageBlob {
merkle: "8cb3466c6e66592c8decaeaa3e399652fbe71dad5c3df1a5e919743a33815567".to_string(),
path: "lib/ghi".to_string(),
used_space_in_blobfs: 0,
};
let package_blob3 = PackageBlob {
merkle: "eabdb84d26416c1821fd8972e0d835eedaf7468e5a9ebe01e5944462411aec70".to_string(),
path: "abc/".to_string(),
used_space_in_blobfs: 0,
};
let expected_blobs = vec![package_blob1, package_blob2, package_blob3];
// Verify if all 3 blobs are available and also if they are sorted.
assert_eq!(actual_package_blobs_base, &expected_blobs);
assert_eq!(actual_package_blobs_cache, &expected_blobs);
// Verify blobs in BlobfsContents
assert_eq!(contents.blobs.0.iter().map(|x| x.clone()).collect::<Vec<_>>(), expected_blobs);
Ok(())
}
fn generate_test_package_manifest_content() -> String {
let content = r#"{
"package": {
"name": "test_package",
"version": "0"
},
"blobs": [
{
"path": "abc/",
"merkle": "eabdb84d26416c1821fd8972e0d835eedaf7468e5a9ebe01e5944462411aec70",
"size": 2048,
"source_path": "../../blobs/eabdb84d26416c1821fd8972e0d835eedaf7468e5a9ebe01e5944462411aec78"
},
{
"path": "bin/def",
"merkle": "7ddff816740d5803358dd4478d8437585e8d5c984b4361817d891807a16ff581",
"size": 188416,
"source_path": "../../blobs/7ddff816740d5803358dd4478d8437585e8d5c984b4361817d891807a16ff581"
},
{
"path": "lib/ghi",
"merkle": "8cb3466c6e66592c8decaeaa3e399652fbe71dad5c3df1a5e919743a33815567",
"size": 692224,
"source_path": "../../blobs/8cb3466c6e66592c8decaeaa3e399652fbe71dad5c3df1a5e919743a33815567"
}
],
"version": "1",
"blob_sources_relative": "file",
"repository": "fuchsia.com"
}
"#;
return content.to_string();
}
fn generate_test_value() -> Value {
json!([
{
"type": "far",
"name": "base-package",
"path": "path/to/base.far",
},
{
"type": "zbi",
"name": "zircon-a",
"path": "path/to/fuchsia.zbi",
"signed": true,
},
{
"type": "vbmeta",
"name": "zircon-a",
"path": "path/to/fuchsia.vbmeta",
},
{
"type": "blk",
"name": "blob",
"path": "path/to/blob.blk",
"contents": {
"packages": {
"base": [],
"cache": [],
},
"maximum_contents_size": None::<u64>,
"blobs": [],
},
},
{
"type": "blk",
"name": "storage-full",
"path": "path/to/fvm.blk",
},
{
"type": "blk",
"name": "storage-sparse",
"path": "path/to/fvm.sparse.blk",
},
{
"type": "blk",
"name": "storage-sparse-blob",
"path": "path/to/fvm.blob.sparse.blk",
},
{
"type": "blk",
"name": "fvm.fastboot",
"path": "path/to/fvm.fastboot.blk",
},
{
"type": "kernel",
"name": "qemu-kernel",
"path": "path/to/qemu/kernel",
},
])
}
}