| // 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 anyhow::{Context, Result}; |
| use camino::{Utf8Path, Utf8PathBuf}; |
| use fuchsia_pkg::PackageManifest; |
| use serde::de::{self, Deserializer}; |
| use serde::ser::Serializer; |
| use serde::{Deserialize, Serialize}; |
| use std::collections::{BTreeSet, HashMap}; |
| use std::fs::File; |
| use utf8_path::path_relative_from; |
| |
| /// A manifest containing a list of images produced by the Image Assembler. |
| /// |
| /// ``` |
| /// use assembly_manifest::AssemblyManifest; |
| /// |
| /// let manifest = AssemblyManifest { |
| /// 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(Clone, Debug, Default, Eq, Hash, PartialEq)] |
| pub struct AssemblyManifest { |
| /// List of images in the manifest. |
| pub images: Vec<Image>, |
| } |
| |
| /// Private helper for serializing the AssemblyManifest. An AssemblyManifest cannot be deserialized |
| /// without going through `try_from_path` in order to require that we use this helper |
| /// which ensure that the paths get relativized/derelativized properly |
| #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] |
| #[serde(transparent)] |
| struct SerializationHelper { |
| images: Vec<Image>, |
| } |
| |
| /// An item in the AssemblyManifest. |
| #[derive(Clone, Debug, Eq, Hash, PartialEq)] |
| pub enum Image { |
| /// Base Package. |
| BasePackage(Utf8PathBuf), |
| |
| /// Zircon Boot Image. |
| ZBI { |
| /// Path to the ZBI image. |
| path: Utf8PathBuf, |
| /// Whether the ZBI is signed. |
| signed: bool, |
| }, |
| |
| /// Verified Boot Metadata. |
| VBMeta(Utf8PathBuf), |
| |
| /// BlobFS image. |
| BlobFS { |
| /// Path to the BlobFS image. |
| path: Utf8PathBuf, |
| /// Contents metadata. |
| contents: BlobfsContents, |
| }, |
| |
| /// Fuchsia Volume Manager. |
| FVM(Utf8PathBuf), |
| |
| /// Sparse FVM. |
| FVMSparse(Utf8PathBuf), |
| |
| /// Fastboot FVM. |
| FVMFastboot(Utf8PathBuf), |
| |
| /// Fxfs. |
| Fxfs { |
| /// Path to the Fxfs image. |
| path: Utf8PathBuf, |
| /// Blob contents metadata. |
| contents: BlobfsContents, |
| }, |
| |
| /// Fxfs in the Android Sparse format. |
| FxfsSparse { |
| /// Path to the Fxfs image. |
| path: Utf8PathBuf, |
| /// Blob contents metadata. |
| contents: BlobfsContents, |
| }, |
| |
| /// Qemu Kernel. |
| QemuKernel(Utf8PathBuf), |
| } |
| |
| impl Image { |
| /// Get the path of the image on the host. |
| pub fn source(&self) -> &Utf8Path { |
| match self { |
| Image::BasePackage(s) => s.as_path(), |
| Image::ZBI { path, signed: _ } => path.as_path(), |
| Image::VBMeta(s) => s.as_path(), |
| Image::BlobFS { path, .. } => path.as_path(), |
| Image::FVM(s) => s.as_path(), |
| Image::FVMSparse(s) => s.as_path(), |
| Image::FVMFastboot(s) => s.as_path(), |
| Image::Fxfs { path, .. } => path.as_path(), |
| Image::FxfsSparse { path, .. } => path.as_path(), |
| Image::QemuKernel(s) => s.as_path(), |
| } |
| } |
| |
| /// Set the path of the image on the host. |
| pub fn set_source(&mut self, source: impl Into<Utf8PathBuf>) { |
| let source = source.into(); |
| match self { |
| Image::BasePackage(s) => *s = source, |
| Image::ZBI { path, signed: _ } => *path = source, |
| Image::VBMeta(s) => *s = source, |
| Image::BlobFS { path, .. } => *path = source, |
| Image::FVM(s) => *s = source, |
| Image::FVMSparse(s) => *s = source, |
| Image::FVMFastboot(s) => *s = source, |
| Image::Fxfs { path, .. } => *path = source, |
| Image::FxfsSparse { path, .. } => *path = source, |
| Image::QemuKernel(s) => *s = source, |
| } |
| } |
| |
| /// For image types that contain blobs, return the contents. |
| pub fn get_blobfs_contents(&self) -> Option<&BlobfsContents> { |
| match self { |
| Image::BlobFS { contents, .. } => Some(contents), |
| Image::Fxfs { contents, .. } => Some(contents), |
| Image::FxfsSparse { contents, .. } => Some(contents), |
| Image::BasePackage(_) |
| | Image::ZBI { .. } |
| | Image::VBMeta(_) |
| | Image::FVM(_) |
| | Image::FVMSparse(_) |
| | Image::FVMFastboot(_) |
| | Image::QemuKernel(_) => None, |
| } |
| } |
| } |
| |
| impl AssemblyManifest { |
| /// Return a new Assembly Manifest with all the paths relativized to a |
| /// target path |
| fn relativize(mut self, base_path: impl AsRef<Utf8Path>) -> Result<AssemblyManifest> { |
| // Change the images list in-place so fields can be added to the AssemblyManifest without |
| // changing this method |
| let mut images = Vec::default(); |
| for image in self.images { |
| match image { |
| Image::BasePackage(path) => { |
| images.push(Image::BasePackage(path_relative_from(path, &base_path)?)) |
| } |
| Image::VBMeta(path) => { |
| images.push(Image::VBMeta(path_relative_from(path, &base_path)?)) |
| } |
| Image::FVM(path) => images.push(Image::FVM(path_relative_from(path, &base_path)?)), |
| Image::FVMSparse(path) => { |
| images.push(Image::FVMSparse(path_relative_from(path, &base_path)?)) |
| } |
| Image::FVMFastboot(path) => { |
| images.push(Image::FVMFastboot(path_relative_from(path, &base_path)?)) |
| } |
| Image::QemuKernel(path) => { |
| images.push(Image::QemuKernel(path_relative_from(path, &base_path)?)) |
| } |
| Image::ZBI { path, signed } => { |
| images.push(Image::ZBI { path: path_relative_from(path, &base_path)?, signed }) |
| } |
| Image::BlobFS { path, contents } => images.push(Image::BlobFS { |
| path: path_relative_from(path, &base_path)?, |
| contents: contents.relativize(&base_path)?, |
| }), |
| Image::Fxfs { path, contents } => images.push(Image::Fxfs { |
| path: path_relative_from(path, &base_path)?, |
| contents: contents.relativize(&base_path)?, |
| }), |
| Image::FxfsSparse { path, contents } => images.push(Image::FxfsSparse { |
| path: path_relative_from(path, &base_path)?, |
| contents: contents.relativize(&base_path)?, |
| }), |
| } |
| } |
| self.images = images; |
| |
| Ok(self) |
| } |
| |
| /// Return a new Assembly manifest with all the paths made joined to the |
| /// provided directory where the manifest is located |
| fn derelativize(mut self, manifest_dir: impl AsRef<Utf8Path>) -> Result<AssemblyManifest> { |
| // Change the images list in-place so fields can be added to the AssemblyManifest without |
| // changing this method |
| let mut images = Vec::default(); |
| for image in self.images { |
| match image { |
| Image::BasePackage(path) => { |
| images.push(Image::BasePackage(manifest_dir.as_ref().join(path))) |
| } |
| Image::VBMeta(path) => images.push(Image::VBMeta(manifest_dir.as_ref().join(path))), |
| Image::FVM(path) => images.push(Image::FVM(manifest_dir.as_ref().join(path))), |
| Image::FVMSparse(path) => { |
| images.push(Image::FVMSparse(manifest_dir.as_ref().join(path))) |
| } |
| Image::FVMFastboot(path) => { |
| images.push(Image::FVMFastboot(manifest_dir.as_ref().join(path))) |
| } |
| Image::QemuKernel(path) => { |
| images.push(Image::QemuKernel(manifest_dir.as_ref().join(path))) |
| } |
| Image::ZBI { path, signed } => { |
| images.push(Image::ZBI { path: manifest_dir.as_ref().join(path), signed }) |
| } |
| Image::BlobFS { path, contents } => images.push(Image::BlobFS { |
| path: manifest_dir.as_ref().join(path), |
| contents: contents.derelativize(&manifest_dir)?, |
| }), |
| Image::Fxfs { path, contents } => images.push(Image::Fxfs { |
| path: manifest_dir.as_ref().join(path), |
| contents: contents.derelativize(&manifest_dir)?, |
| }), |
| Image::FxfsSparse { path, contents } => images.push(Image::FxfsSparse { |
| path: manifest_dir.as_ref().join(path), |
| contents: contents.derelativize(&manifest_dir)?, |
| }), |
| } |
| } |
| self.images = images; |
| |
| Ok(self) |
| } |
| |
| /// Load an AssemblyManifest from a path on disk, handling path |
| /// relativization |
| pub fn try_load_from(path: impl AsRef<Utf8Path>) -> Result<Self> { |
| let deserialized: SerializationHelper = assembly_util::read_config(path.as_ref())?; |
| let manifest = AssemblyManifest { images: deserialized.images }; |
| manifest.derelativize(path.as_ref().parent().context("Invalid path")?) |
| } |
| |
| /// Write a product bundle to a directory on disk at `path`. |
| /// Make the paths recorded in the file relative to the file's location |
| /// so it is portable. |
| pub fn write(self, path: impl AsRef<Utf8Path>) -> Result<()> { |
| let path = path.as_ref(); |
| // Relativize the paths in the Assembly Manifest |
| let assembly_manifest = |
| self.relativize(path.parent().with_context(|| format!("Invalid output path {path}"))?)?; |
| |
| // Write the images manifest. |
| let images_json = File::create(path).context("Creating assembly manifest")?; |
| serde_json::to_writer_pretty( |
| images_json, |
| &SerializationHelper { images: assembly_manifest.images }, |
| ) |
| .context("Writing assembly manifest")?; |
| Ok(()) |
| } |
| } |
| |
| /// A specific Image type. |
| |
| #[derive(Debug, Serialize)] |
| struct ImageSerializeHelper<'a> { |
| #[serde(rename = "type")] |
| partition_type: &'a str, |
| name: &'a str, |
| path: &'a Utf8Path, |
| #[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::FVMFastboot(path) => ImageSerializeHelper { |
| partition_type: "blk", |
| name: "fvm.fastboot", |
| path, |
| signed: None, |
| contents: None, |
| }, |
| Image::Fxfs { path, contents } => ImageSerializeHelper { |
| partition_type: "fxfs-blk", |
| name: "storage-full", |
| path, |
| signed: None, |
| contents: Some(ImageContentsSerializeHelper::Blobfs(contents)), |
| }, |
| Image::FxfsSparse { path, contents } => ImageSerializeHelper { |
| partition_type: "blk", |
| name: "fxfs.fastboot", |
| path, |
| signed: None, |
| contents: Some(ImageContentsSerializeHelper::Blobfs(contents)), |
| }, |
| 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(Clone, Debug, Default, Deserialize, Eq, Hash, 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>, |
| } |
| |
| /// Contains unique set of blobs. |
| #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] |
| #[serde(transparent)] |
| pub struct PackageSetBlobInfo(pub BTreeSet<PackageBlob>); |
| |
| impl BlobfsContents { |
| /// Add base package info into BlobfsContents |
| pub fn add_base_package( |
| &mut self, |
| path: impl AsRef<Utf8Path>, |
| merkle_size_map: &HashMap<String, u64>, |
| ) -> anyhow::Result<()> { |
| Self::add_package(&mut self.packages.base, path, merkle_size_map)?; |
| Ok(()) |
| } |
| |
| /// Add cache package info into BlobfsContents |
| pub fn add_cache_package( |
| &mut self, |
| path: impl AsRef<Utf8Path>, |
| merkle_size_map: &HashMap<String, u64>, |
| ) -> anyhow::Result<()> { |
| Self::add_package(&mut self.packages.cache, path, merkle_size_map)?; |
| Ok(()) |
| } |
| |
| fn add_package_blobs( |
| subpackage_name: Option<String>, |
| package_manifest: PackageManifest, |
| package_blobs: &mut Vec<PackageBlob>, |
| merkle_size_map: &HashMap<String, u64>, |
| ) -> anyhow::Result<()> { |
| let (blobs, subpackages) = package_manifest.into_blobs_and_subpackages(); |
| for blob in blobs { |
| let path = if let Some(subpackage) = &subpackage_name { |
| format!("{}/{}", subpackage, &blob.path) |
| } else { |
| blob.path.to_string() |
| }; |
| package_blobs.push(PackageBlob { |
| merkle: blob.merkle.to_string(), |
| path, |
| used_space_in_blobfs: *merkle_size_map.get(&blob.merkle.to_string()).with_context( |
| || format!("Blob merkle not found. {} {}", blob.path, blob.merkle), |
| )?, |
| }); |
| } |
| for subpackage in subpackages { |
| let subpackage_name = if let Some(parent_subpackage) = &subpackage_name { |
| format!("{}/{}", parent_subpackage, subpackage.name) |
| } else { |
| subpackage.name |
| }; |
| Self::add_package_blobs( |
| Some(subpackage_name), |
| PackageManifest::try_load_from(subpackage.manifest_path)?, |
| package_blobs, |
| merkle_size_map, |
| )?; |
| } |
| Ok(()) |
| } |
| |
| fn add_package( |
| package_set: &mut PackageSetMetadata, |
| path: impl AsRef<Utf8Path>, |
| merkle_size_map: &HashMap<String, u64>, |
| ) -> 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![]; |
| Self::add_package_blobs(None, package_manifest, &mut package_blobs, merkle_size_map) |
| .with_context(|| format!("adding package: {manifest}"))?; |
| package_blobs.sort(); |
| package_set.0.push(PackageMetadata { name, manifest, blobs: package_blobs }); |
| Ok(()) |
| } |
| |
| /// Relativize all manifest locations in the BlobFsContents to the output file's location |
| pub fn relativize(mut self, base_path: impl AsRef<Utf8Path>) -> Result<BlobfsContents> { |
| // Modify in-place so we can add fields to the struct without updating this code |
| for package in self.packages.base.0.iter_mut() { |
| package.manifest = path_relative_from(&package.manifest, &base_path)? |
| } |
| for package in self.packages.cache.0.iter_mut() { |
| package.manifest = path_relative_from(&package.manifest, &base_path)? |
| } |
| Ok(self) |
| } |
| |
| /// Join all manifest paths in the BlobFsContents to the location of the file it is serialized in |
| fn derelativize(mut self, manifest_dir: impl AsRef<Utf8Path>) -> Result<BlobfsContents> { |
| // Modify in-place so we can add fields without updating this code |
| for package in self.packages.base.0.iter_mut() { |
| package.manifest = manifest_dir.as_ref().join(&package.manifest) |
| } |
| for package in self.packages.cache.0.iter_mut() { |
| package.manifest = manifest_dir.as_ref().join(&package.manifest) |
| } |
| Ok(self) |
| } |
| } |
| |
| /// Metadata on packages included in a given image. |
| #[derive(Clone, Debug, Default, Deserialize, Eq, Hash, 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(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)] |
| #[serde(transparent)] |
| pub struct PackageSetMetadata(pub Vec<PackageMetadata>); |
| |
| /// Metadata on a single package included in a given image. |
| #[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)] |
| pub struct PackageMetadata { |
| /// The package's name. |
| pub name: String, |
| /// Path to the package's manifest. |
| pub manifest: Utf8PathBuf, |
| /// List of blobs in this package. |
| pub blobs: Vec<PackageBlob>, |
| } |
| |
| #[derive(Clone, Debug, Ord, PartialOrd, PartialEq, Eq, Hash, 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: Utf8PathBuf, |
| 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>, |
| { |
| // Due to historical reasons, there are images where "name" has come to |
| // parameterize the image type itself. Outside of those cases, the name |
| // is just a human-readable bit of metadata identifying a given |
| // instance of that image and so we disregard such incoming values. |
| // These images are renamed with with their canonical Assembly names |
| // (e.g., "zircon-a") at serialization-time. |
| let helper = ImageDeserializeHelper::deserialize(deserializer)?; |
| match (&helper.partition_type[..], &helper.name[..], &helper.signed) { |
| ("far", "base-package", None) => Ok(Image::BasePackage(helper.path)), |
| ("zbi", _, signed) => { |
| Ok(Image::ZBI { path: helper.path, signed: signed.is_some_and(|s| s) }) |
| } |
| ("vbmeta", _, 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)), |
| ("fxfs-blk", "storage-full", None) => { |
| if let Some(contents) = helper.contents { |
| let ImageContentsDeserializeHelper::Blobfs(contents) = contents; |
| Ok(Image::Fxfs { path: helper.path, contents }) |
| } else { |
| Err(de::Error::missing_field("contents")) |
| } |
| } |
| ("blk", "fxfs.fastboot", None) => { |
| if let Some(contents) = helper.contents { |
| let ImageContentsDeserializeHelper::Blobfs(contents) = contents; |
| Ok(Image::FxfsSparse { path: helper.path, contents }) |
| } else { |
| Err(de::Error::missing_field("contents")) |
| } |
| } |
| ("blk", "storage-sparse", None) => Ok(Image::FVMSparse(helper.path)), |
| ("blk", "fvm.fastboot", None) => Ok(Image::FVMFastboot(helper.path)), |
| ("kernel", _, None) => Ok(Image::QemuKernel(helper.path)), |
| (partition_type, name, _) => Err(de::Error::unknown_variant( |
| &format!("({partition_type}, {name})"), |
| &[ |
| "(far, base-package)", |
| "(zbi, _)", |
| "(vbmeta, _)", |
| "(blk, blob)", |
| "(blk, storage-full)", |
| "(blk, storage-sparse)", |
| "(blk, fvm.fastboot)", |
| "(kernel, _)", |
| ], |
| )), |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use serde_json::{json, Value}; |
| use std::io::Write; |
| use tempfile::{tempdir, NamedTempFile}; |
| |
| #[test] |
| fn image_source() { |
| let mut image = Image::FVM("path/to/fvm.blk".into()); |
| assert_eq!(image.source(), &Utf8PathBuf::from("path/to/fvm.blk")); |
| image.set_source("path/to/fvm2.blk"); |
| assert_eq!(image.source(), &Utf8PathBuf::from("path/to/fvm2.blk")); |
| } |
| |
| #[test] |
| fn relativize() { |
| let manifest = AssemblyManifest { |
| images: vec![ |
| Image::BasePackage("base.far".into()), |
| Image::ZBI { path: "fuchsia.zbi".into(), signed: true }, |
| Image::VBMeta("fuchsia.vbmeta".into()), |
| Image::BlobFS { path: "blob.blk".into(), contents: Default::default() }, |
| Image::FVM("fvm.blk".into()), |
| Image::FVMSparse("fvm.sparse.blk".into()), |
| Image::FVMFastboot("fvm.fastboot.blk".into()), |
| Image::QemuKernel("qemu/kernel".into()), |
| ], |
| }; |
| |
| let relativized_manifest = |
| generate_test_manifest().relativize(Utf8PathBuf::from("path/to")).unwrap(); |
| assert_eq!(relativized_manifest, manifest); |
| } |
| |
| #[test] |
| fn relativize_fxfs() { |
| let manifest = AssemblyManifest { |
| images: vec![ |
| Image::BasePackage("base.far".into()), |
| Image::ZBI { path: "fuchsia.zbi".into(), signed: true }, |
| Image::VBMeta("fuchsia.vbmeta".into()), |
| Image::Fxfs { path: "fxfs.blk".into(), contents: Default::default() }, |
| Image::FxfsSparse { path: "fxfs.sparse.blk".into(), contents: Default::default() }, |
| Image::QemuKernel("qemu/kernel".into()), |
| ], |
| }; |
| |
| let relativized_manifest = |
| generate_test_manifest_fxfs().relativize(Utf8PathBuf::from("path/to")).unwrap(); |
| assert_eq!(relativized_manifest, manifest); |
| } |
| |
| #[test] |
| fn derelativize() { |
| let manifest = AssemblyManifest { |
| images: vec![ |
| Image::BasePackage("base.far".into()), |
| Image::ZBI { path: "fuchsia.zbi".into(), signed: true }, |
| Image::VBMeta("fuchsia.vbmeta".into()), |
| Image::BlobFS { path: "blob.blk".into(), contents: Default::default() }, |
| Image::FVM("fvm.blk".into()), |
| Image::FVMSparse("fvm.sparse.blk".into()), |
| Image::FVMFastboot("fvm.fastboot.blk".into()), |
| Image::QemuKernel("qemu/kernel".into()), |
| ], |
| }; |
| |
| let derelativized_manifest = manifest.derelativize(Utf8PathBuf::from("path/to")).unwrap(); |
| let manifest = generate_test_manifest(); |
| assert_eq!(manifest, derelativized_manifest); |
| } |
| |
| #[test] |
| fn derelativize_fxfs() { |
| let manifest = AssemblyManifest { |
| images: vec![ |
| Image::BasePackage("base.far".into()), |
| Image::ZBI { path: "fuchsia.zbi".into(), signed: true }, |
| Image::VBMeta("fuchsia.vbmeta".into()), |
| Image::Fxfs { path: "fxfs.blk".into(), contents: Default::default() }, |
| Image::FxfsSparse { path: "fxfs.sparse.blk".into(), contents: Default::default() }, |
| Image::QemuKernel("qemu/kernel".into()), |
| ], |
| }; |
| |
| let derelativized_manifest = manifest.derelativize(Utf8PathBuf::from("path/to")).unwrap(); |
| let manifest = generate_test_manifest_fxfs(); |
| assert_eq!(manifest, derelativized_manifest); |
| } |
| |
| #[test] |
| fn serialize() { |
| let manifest = AssemblyManifest { |
| 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::FVMFastboot("path/to/fvm.fastboot.blk".into()), |
| Image::QemuKernel("path/to/qemu/kernel".into()), |
| ], |
| }; |
| |
| assert_eq!( |
| generate_test_value(), |
| serde_json::to_value(SerializationHelper { images: manifest.images }).unwrap() |
| ); |
| } |
| |
| #[test] |
| fn serialize_fxfs() { |
| let manifest = AssemblyManifest { |
| 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::Fxfs { path: "path/to/fxfs.blk".into(), contents: Default::default() }, |
| Image::FxfsSparse { |
| path: "path/to/fxfs.sparse.blk".into(), |
| contents: Default::default(), |
| }, |
| Image::QemuKernel("path/to/qemu/kernel".into()), |
| ], |
| }; |
| |
| assert_eq!( |
| generate_test_value_fxfs(), |
| serde_json::to_value(SerializationHelper { images: manifest.images }).unwrap() |
| ); |
| } |
| |
| #[test] |
| fn serialize_unsigned_zbi() { |
| let manifest = AssemblyManifest { |
| 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(SerializationHelper { images: manifest.images }).unwrap() |
| ); |
| } |
| |
| #[test] |
| fn deserialize() { |
| let manifest: AssemblyManifest = generate_test_manifest(); |
| assert_eq!(manifest.images.len(), 8); |
| |
| 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::FVMFastboot(path) => ("path/to/fvm.fastboot.blk", path), |
| Image::QemuKernel(path) => ("path/to/qemu/kernel", path), |
| _ => panic!("Unexpected item {:?}", image), |
| }; |
| assert_eq!(&Utf8PathBuf::from(expected), actual); |
| } |
| } |
| |
| #[test] |
| fn deserialize_fxfs() { |
| let manifest: AssemblyManifest = generate_test_manifest_fxfs(); |
| assert_eq!(manifest.images.len(), 6); |
| |
| 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::Fxfs { path, contents } => { |
| assert_eq!(contents, &BlobfsContents::default()); |
| ("path/to/fxfs.blk", path) |
| } |
| Image::FxfsSparse { path, contents } => { |
| assert_eq!(contents, &BlobfsContents::default()); |
| ("path/to/fxfs.sparse.blk", path) |
| } |
| Image::QemuKernel(path) => ("path/to/qemu/kernel", path), |
| _ => panic!("Unexpected item {:?}", image), |
| }; |
| assert_eq!(&Utf8PathBuf::from(expected), actual); |
| } |
| } |
| |
| #[test] |
| fn deserialize_invalid() { |
| let invalid = json!([ |
| { |
| "type": "far-invalid", |
| "name": "base-package", |
| "path": "path/to/base.far", |
| }, |
| ]); |
| let result: Result<SerializationHelper, _> = serde_json::from_value(invalid); |
| assert!(result.unwrap_err().is_data()); |
| } |
| |
| #[test] |
| fn deserialize_zbi_with_arbitrary_name() { |
| let value = json!([ |
| { |
| "type": "zbi", |
| "name": "my-zbi", |
| "path": "path/to/my.zbi", |
| } |
| ]); |
| |
| let expected = AssemblyManifest { |
| images: vec![Image::ZBI { path: "path/to/my.zbi".into(), signed: false }], |
| }; |
| |
| let result: Result<SerializationHelper, _> = serde_json::from_value(value); |
| assert!(result.is_ok()); |
| assert_eq!(result.unwrap(), SerializationHelper { images: expected.images }); |
| } |
| |
| #[test] |
| fn deserialize_vbmeta_with_arbitrary_name() { |
| let value = json!([ |
| { |
| "type": "vbmeta", |
| "name": "my-vbmeta", |
| "path": "path/to/my.vbmeta", |
| } |
| ]); |
| |
| let expected = AssemblyManifest { images: vec![Image::VBMeta("path/to/my.vbmeta".into())] }; |
| |
| let result: Result<SerializationHelper, _> = serde_json::from_value(value); |
| assert!(result.is_ok()); |
| assert_eq!(result.unwrap(), SerializationHelper { images: expected.images }); |
| } |
| |
| #[test] |
| fn deserialize_qemu_kernel_with_arbitrary_name() { |
| let value = json!([ |
| { |
| "type": "kernel", |
| "name": "my-qemu-kernel", |
| "path": "path/to/my-qemu-kernel.bin", |
| } |
| ]); |
| |
| let expected = AssemblyManifest { |
| images: vec![Image::QemuKernel("path/to/my-qemu-kernel.bin".into())], |
| }; |
| |
| let result: Result<SerializationHelper, _> = serde_json::from_value(value); |
| assert!(result.is_ok()); |
| assert_eq!(result.unwrap(), SerializationHelper { images: expected.images }); |
| } |
| |
| #[test] |
| fn test_blobfs_contents_add_base_or_cache_package() -> anyhow::Result<()> { |
| let content = generate_test_package_manifest_content(); |
| let super_content = |
| generate_test_super_package_manifest_content("package_manifest_temp_file.json"); |
| |
| let mut package_manifest_temp_file = NamedTempFile::new()?; |
| let mut super_package_manifest_temp_file = NamedTempFile::new()?; |
| let dir = tempdir().unwrap(); |
| let root = Utf8Path::from_path(dir.path()).unwrap(); |
| write!(package_manifest_temp_file, "{content}")?; |
| write!(super_package_manifest_temp_file, "{super_content}")?; |
| let path = package_manifest_temp_file.into_temp_path(); |
| path.persist(dir.path().join("package_manifest_temp_file.json"))?; |
| let path = super_package_manifest_temp_file.into_temp_path(); |
| path.persist(dir.path().join("super_package_manifest_temp_file.json"))?; |
| |
| let mut contents = BlobfsContents::default(); |
| let mut merkle_size_map: HashMap<String, u64> = HashMap::new(); |
| merkle_size_map.insert( |
| "7ddff816740d5803358dd4478d8437585e8d5c984b4361817d891807a16ff581".to_string(), |
| 10, |
| ); |
| merkle_size_map.insert( |
| "8cb3466c6e66592c8decaeaa3e399652fbe71dad5c3df1a5e919743a33815567".to_string(), |
| 20, |
| ); |
| merkle_size_map.insert( |
| "eabdb84d26416c1821fd8972e0d835eedaf7468e5a9ebe01e5944462411aec70".to_string(), |
| 30, |
| ); |
| contents |
| .add_base_package(root.join("package_manifest_temp_file.json"), &merkle_size_map)?; |
| contents.add_base_package( |
| root.join("super_package_manifest_temp_file.json"), |
| &merkle_size_map, |
| )?; |
| contents |
| .add_cache_package(root.join("package_manifest_temp_file.json"), &merkle_size_map)?; |
| let actual_package_blobs_base = &(contents.packages.base.0[0].blobs); |
| let actual_package_blobs_super_base = &(contents.packages.base.0[1].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: 10, |
| }; |
| |
| let package_blob2 = PackageBlob { |
| merkle: "8cb3466c6e66592c8decaeaa3e399652fbe71dad5c3df1a5e919743a33815567".to_string(), |
| path: "lib/ghi".to_string(), |
| used_space_in_blobfs: 20, |
| }; |
| |
| let package_blob3 = PackageBlob { |
| merkle: "eabdb84d26416c1821fd8972e0d835eedaf7468e5a9ebe01e5944462411aec70".to_string(), |
| path: "abc/".to_string(), |
| used_space_in_blobfs: 30, |
| }; |
| |
| let super_package_blob1 = PackageBlob { |
| merkle: "7ddff816740d5803358dd4478d8437585e8d5c984b4361817d891807a16ff581".to_string(), |
| path: "my-subpackage/bin/def".to_string(), |
| used_space_in_blobfs: 10, |
| }; |
| |
| let super_package_blob2 = PackageBlob { |
| merkle: "8cb3466c6e66592c8decaeaa3e399652fbe71dad5c3df1a5e919743a33815567".to_string(), |
| path: "my-subpackage/lib/ghi".to_string(), |
| used_space_in_blobfs: 20, |
| }; |
| |
| let super_package_blob3 = PackageBlob { |
| merkle: "eabdb84d26416c1821fd8972e0d835eedaf7468e5a9ebe01e5944462411aec70".to_string(), |
| path: "my-subpackage/abc/".to_string(), |
| used_space_in_blobfs: 30, |
| }; |
| |
| let expected_blobs = vec![package_blob1, package_blob2, package_blob3]; |
| let expected_super_blobs = |
| vec![super_package_blob1, super_package_blob2, super_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); |
| assert_eq!(actual_package_blobs_super_base, &expected_super_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" |
| } |
| "#; |
| content.to_string() |
| } |
| |
| fn generate_test_super_package_manifest_content(subpackage_manifest_path: &str) -> String { |
| format!( |
| r#"{{ |
| "package": {{ |
| "name": "super_package", |
| "version": "0" |
| }}, |
| "blobs": [], |
| "subpackages": [ |
| {{ |
| "name": "my-subpackage", |
| "merkle": "eabdb84d26416c1821fd8972e0d835eedaf7468e5a9ebe01e5944462411aec70", |
| "manifest_path": "{subpackage_manifest_path}" |
| }} |
| ], |
| "version": "1", |
| "blob_sources_relative": "file", |
| "repository": "fuchsia.com" |
| }} |
| "# |
| ) |
| } |
| |
| fn generate_test_manifest() -> AssemblyManifest { |
| AssemblyManifest { |
| images: serde_json::from_value::<SerializationHelper>(generate_test_value()) |
| .unwrap() |
| .images, |
| } |
| } |
| |
| fn generate_test_manifest_fxfs() -> AssemblyManifest { |
| AssemblyManifest { |
| images: serde_json::from_value::<SerializationHelper>(generate_test_value_fxfs()) |
| .unwrap() |
| .images, |
| } |
| } |
| |
| 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> |
| }, |
| }, |
| { |
| "type": "blk", |
| "name": "storage-full", |
| "path": "path/to/fvm.blk", |
| }, |
| { |
| "type": "blk", |
| "name": "storage-sparse", |
| "path": "path/to/fvm.sparse.blk", |
| }, |
| { |
| "type": "blk", |
| "name": "fvm.fastboot", |
| "path": "path/to/fvm.fastboot.blk", |
| }, |
| { |
| "type": "kernel", |
| "name": "qemu-kernel", |
| "path": "path/to/qemu/kernel", |
| }, |
| ]) |
| } |
| |
| fn generate_test_value_fxfs() -> 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": "fxfs-blk", |
| "name": "storage-full", |
| "path": "path/to/fxfs.blk", |
| "contents": { |
| "packages": { |
| "base": [], |
| "cache": [], |
| }, |
| "maximum_contents_size": None::<u64> |
| }, |
| }, |
| { |
| "type": "blk", |
| "name": "fxfs.fastboot", |
| "path": "path/to/fxfs.sparse.blk", |
| "contents": { |
| "packages": { |
| "base": [], |
| "cache": [], |
| }, |
| "maximum_contents_size": None::<u64> |
| }, |
| }, |
| { |
| "type": "kernel", |
| "name": "qemu-kernel", |
| "path": "path/to/qemu/kernel", |
| }, |
| ]) |
| } |
| } |