| // Copyright 2022 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 camino::Utf8PathBuf; |
| use serde::{Deserialize, Serialize}; |
| use std::convert::TryFrom; |
| use std::fmt; |
| use std::io::Read; |
| use std::path::PathBuf; |
| use std::str::FromStr; |
| |
| /// The configuration file specifying which images to generate and how. |
| #[derive(Serialize, Deserialize, Debug, Default)] |
| pub struct ImagesConfig { |
| /// A list of images to generate. |
| #[serde(default)] |
| pub images: Vec<Image>, |
| } |
| |
| /// An image to generate. |
| #[derive(Serialize, Deserialize, Debug)] |
| #[serde(tag = "type")] |
| pub enum Image { |
| /// A FVM image. |
| #[serde(rename = "fvm")] |
| Fvm(Fvm), |
| |
| /// A ZBI image. |
| #[serde(rename = "zbi")] |
| Zbi(Zbi), |
| |
| /// A VBMeta image. |
| #[serde(rename = "vbmeta")] |
| VBMeta(VBMeta), |
| } |
| |
| /// Parameters describing how to generate the ZBI. |
| #[derive(Serialize, Deserialize, Debug)] |
| pub struct Zbi { |
| /// The name to give the image file. |
| #[serde(default = "default_zbi_name")] |
| pub name: String, |
| |
| /// The compression format for the ZBI. |
| #[serde(default = "default_zbi_compression")] |
| pub compression: ZbiCompression, |
| |
| /// An optional script to post-process the ZBI. |
| /// This is often used to prepare the ZBI for flashing/updating. |
| #[serde(default)] |
| pub postprocessing_script: Option<PostProcessingScript>, |
| } |
| |
| fn default_zbi_name() -> String { |
| "fuchsia".into() |
| } |
| |
| fn default_zbi_compression() -> ZbiCompression { |
| ZbiCompression::ZStd |
| } |
| |
| /// The compression format for the ZBI. |
| #[derive(Serialize, Deserialize, Debug, PartialEq)] |
| pub enum ZbiCompression { |
| /// zstd compression. |
| #[serde(rename = "zstd")] |
| ZStd, |
| |
| /// zstd.max compression. |
| #[serde(rename = "zstd.max")] |
| ZStdMax, |
| } |
| |
| impl FromStr for ZbiCompression { |
| type Err = anyhow::Error; |
| fn from_str(s: &str) -> Result<Self> { |
| zbi_compression_from_str(s) |
| } |
| } |
| |
| impl TryFrom<&str> for ZbiCompression { |
| type Error = anyhow::Error; |
| fn try_from(s: &str) -> Result<Self> { |
| zbi_compression_from_str(s) |
| } |
| } |
| |
| impl fmt::Display for ZbiCompression { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| write!( |
| f, |
| "{}", |
| match self { |
| ZbiCompression::ZStd => "zstd", |
| ZbiCompression::ZStdMax => "zstd.max", |
| } |
| ) |
| } |
| } |
| |
| fn zbi_compression_from_str(s: &str) -> Result<ZbiCompression> { |
| match s { |
| "zstd" => Ok(ZbiCompression::ZStd), |
| "zstd.max" => Ok(ZbiCompression::ZStdMax), |
| invalid => Err(anyhow!("invalid zbi compression: {}", invalid)), |
| } |
| } |
| |
| /// A script to process the ZBI after it is constructed. |
| #[derive(Serialize, Deserialize, Debug)] |
| pub struct PostProcessingScript { |
| /// The path to the script on host. |
| /// This script _musts_ take the following arguments: |
| /// -z <path to ZBI> |
| /// -o <output path> |
| /// -B <build directory, relative to script's source directory> |
| pub path: PathBuf, |
| |
| /// Additional arguments to pass to the script after the above arguments. |
| #[serde(default)] |
| pub args: Vec<String>, |
| } |
| |
| /// The parameters describing how to create a VBMeta image. |
| #[derive(Serialize, Deserialize, Debug)] |
| pub struct VBMeta { |
| /// The name to give the image file. |
| #[serde(default = "default_vbmeta_name")] |
| pub name: String, |
| |
| /// Path on host to the key for signing VBMeta. |
| pub key: Utf8PathBuf, |
| |
| /// Path on host to the key metadata to add to the VBMeta. |
| pub key_metadata: Utf8PathBuf, |
| |
| /// Optional descriptors to add to the VBMeta image. |
| #[serde(default)] |
| pub additional_descriptors: Vec<VBMetaDescriptor>, |
| } |
| |
| fn default_vbmeta_name() -> String { |
| "fuchsia".into() |
| } |
| |
| /// The parameters of a VBMeta descriptor to add to a VBMeta image. |
| #[derive(Serialize, Deserialize, Debug)] |
| pub struct VBMetaDescriptor { |
| /// Name of the partition. |
| pub name: String, |
| |
| /// Size of the partition in bytes. |
| pub size: u64, |
| |
| /// Custom VBMeta flags to add. |
| pub flags: u32, |
| |
| /// Minimum AVB version to add. |
| pub min_avb_version: String, |
| } |
| |
| /// The parameters describing how to create a FVM image. |
| #[derive(Serialize, Deserialize, Debug, Clone)] |
| pub struct Fvm { |
| /// The size of a slice within the FVM. |
| #[serde(default = "default_fvm_slice_size")] |
| pub slice_size: u64, |
| |
| /// The list of filesystems to generate that can be added to the outputs. |
| pub filesystems: Vec<FvmFilesystem>, |
| |
| /// The FVM images to generate. |
| pub outputs: Vec<FvmOutput>, |
| } |
| |
| fn default_fvm_slice_size() -> u64 { |
| 8388608 |
| } |
| |
| /// A single FVM filesystem that can be added to multiple outputs. |
| #[derive(Serialize, Deserialize, Debug, Clone)] |
| #[serde(tag = "type")] |
| pub enum FvmFilesystem { |
| /// A blobfs volume for holding blobs. |
| #[serde(rename = "blobfs")] |
| BlobFS(BlobFS), |
| |
| /// A minfs volume for holding data. |
| #[serde(rename = "minfs")] |
| MinFS(MinFS), |
| |
| /// An empty data partition. |
| /// This reserves the data volume, which will be formatted as fxfs/minfs on boot. |
| // TODO(fxbug.dev/85134): Remove empty-minfs alias after updating sdk-integration. |
| #[serde(rename = "empty-data", alias = "empty-minfs")] |
| EmptyData(EmptyData), |
| |
| /// Reserved slices in the FVM. |
| #[serde(rename = "reserved")] |
| Reserved(Reserved), |
| } |
| |
| /// Configuration for building a BlobFS volume. |
| #[derive(Serialize, Deserialize, Debug, Clone)] |
| pub struct BlobFS { |
| /// The name of the volume in the FVM. |
| #[serde(default = "default_blobfs_name")] |
| pub name: String, |
| |
| /// Optional deprecated layout. |
| #[serde(default = "default_blobfs_layout")] |
| pub layout: BlobFSLayout, |
| |
| /// Reserve |minimum_data_bytes| and |minimum_inodes| in the FVM, and ensure |
| /// that the final reserved size does not exceed |maximum_bytes|. |
| #[serde(default)] |
| pub maximum_bytes: Option<u64>, |
| |
| /// Reserve space for at least this many data bytes. |
| #[serde(default)] |
| pub minimum_data_bytes: Option<u64>, |
| |
| /// Reserved space for this many inodes. |
| #[serde(default)] |
| pub minimum_inodes: Option<u64>, |
| |
| /// Maximum amount of contents for an assembled blobfs. |
| #[serde(default)] |
| pub maximum_contents_size: Option<u64>, |
| } |
| |
| fn default_blobfs_name() -> String { |
| "blob".into() |
| } |
| |
| fn default_blobfs_layout() -> BlobFSLayout { |
| BlobFSLayout::Compact |
| } |
| |
| /// Configuration for building a MinFS volume. |
| #[derive(Serialize, Deserialize, Debug, Clone)] |
| pub struct MinFS { |
| /// The name of the volume in the FVM. |
| #[serde(default = "default_data_name")] |
| pub name: String, |
| |
| /// Reserve |minimum_data_bytes| and |minimum_inodes| in the FVM, and ensure |
| /// that the final reserved size does not exceed |maximum_bytes|. |
| #[serde(default)] |
| pub maximum_bytes: Option<u64>, |
| |
| /// Reserve space for at least this many data bytes. |
| #[serde(default)] |
| pub minimum_data_bytes: Option<u64>, |
| |
| /// Reserved space for this many inodes. |
| #[serde(default)] |
| pub minimum_inodes: Option<u64>, |
| } |
| |
| fn default_data_name() -> String { |
| "data".into() |
| } |
| |
| /// Configuration for building an EmptyData volume. |
| #[derive(Serialize, Deserialize, Debug, Clone)] |
| pub struct EmptyData { |
| /// The name of the volume in the FVM. |
| #[serde(default = "default_data_name")] |
| pub name: String, |
| } |
| |
| /// Configuration for building a Reserved volume. |
| #[derive(Serialize, Deserialize, Debug, Clone)] |
| pub struct Reserved { |
| /// The name of the volume in the FVM. |
| #[serde(default = "default_reserved_name")] |
| pub name: String, |
| |
| /// The number of slices to reserve. |
| pub slices: u64, |
| } |
| |
| fn default_reserved_name() -> String { |
| "internal".into() |
| } |
| |
| /// The internal layout of blobfs. |
| #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] |
| pub enum BlobFSLayout { |
| /// A more compact layout than DeprecatedPadded. |
| #[serde(rename = "compact")] |
| Compact, |
| |
| /// A layout that is deprecated, but kept for compatibility reasons. |
| #[serde(rename = "deprecated_padded")] |
| DeprecatedPadded, |
| } |
| |
| impl FromStr for BlobFSLayout { |
| type Err = anyhow::Error; |
| fn from_str(s: &str) -> Result<Self> { |
| blobfs_layout_from_str(s) |
| } |
| } |
| |
| impl TryFrom<&str> for BlobFSLayout { |
| type Error = anyhow::Error; |
| fn try_from(s: &str) -> Result<Self> { |
| blobfs_layout_from_str(s) |
| } |
| } |
| |
| impl fmt::Display for BlobFSLayout { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| write!( |
| f, |
| "{}", |
| match self { |
| BlobFSLayout::Compact => "compact", |
| BlobFSLayout::DeprecatedPadded => "deprecated_padded", |
| } |
| ) |
| } |
| } |
| |
| fn blobfs_layout_from_str(s: &str) -> Result<BlobFSLayout> { |
| match s { |
| "compact" => Ok(BlobFSLayout::Compact), |
| "deprecated_padded" => Ok(BlobFSLayout::DeprecatedPadded), |
| _ => Err(anyhow!("invalid blobfs layout")), |
| } |
| } |
| |
| /// A FVM image to generate with a list of filesystems. |
| #[derive(Serialize, Deserialize, Debug, Clone)] |
| #[serde(tag = "type")] |
| pub enum FvmOutput { |
| /// The default FVM type with no modifications. |
| #[serde(rename = "standard")] |
| Standard(StandardFvm), |
| |
| /// A FVM that is compressed sparse. |
| #[serde(rename = "sparse")] |
| Sparse(SparseFvm), |
| |
| /// A FVM prepared for a Nand partition. |
| #[serde(rename = "nand")] |
| Nand(NandFvm), |
| } |
| |
| impl std::fmt::Display for FvmOutput { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| let (fvm_type, name) = match self { |
| FvmOutput::Standard(fvm) => ("Standard", &fvm.name), |
| FvmOutput::Sparse(fvm) => ("Sparse", &fvm.name), |
| FvmOutput::Nand(fvm) => ("Nand", &fvm.name), |
| }; |
| f.write_fmt(format_args!("Fvm::{}(\"{}\")", fvm_type, name)) |
| } |
| } |
| |
| /// The default FVM type with no modifications. |
| #[derive(Serialize, Deserialize, Debug, Clone)] |
| pub struct StandardFvm { |
| /// The name to give the file. |
| pub name: String, |
| |
| /// The filesystems to include in the FVM. |
| pub filesystems: Vec<String>, |
| |
| /// Whether to compress the FVM. |
| #[serde(default)] |
| pub compress: bool, |
| |
| /// Shrink the FVM to fit exactly the contents. |
| #[serde(default)] |
| pub resize_image_file_to_fit: bool, |
| |
| /// After the optional resize, truncate the file to this length. |
| #[serde(default)] |
| pub truncate_to_length: Option<u64>, |
| } |
| |
| /// A FVM that is compressed sparse. |
| #[derive(Serialize, Deserialize, Debug, Clone)] |
| pub struct SparseFvm { |
| /// The name to give the file. |
| pub name: String, |
| |
| /// The filesystems to include in the FVM. |
| pub filesystems: Vec<String>, |
| |
| /// The maximum size the FVM can expand to at runtime. |
| /// This sets the amount of slice metadata to allocate during construction, |
| /// which cannot be modified at runtime. |
| #[serde(default)] |
| pub max_disk_size: Option<u64>, |
| } |
| |
| /// A FVM prepared for a Nand partition. |
| #[derive(Serialize, Deserialize, Debug, Clone)] |
| pub struct NandFvm { |
| /// The name to give the file. |
| pub name: String, |
| |
| /// The filesystems to include in the FVM. |
| #[serde(default)] |
| pub filesystems: Vec<String>, |
| |
| /// The maximum size the FVM can expand to at runtime. |
| /// This sets the amount of slice metadata to allocate during construction, |
| /// which cannot be modified at runtime. |
| #[serde(default)] |
| pub max_disk_size: Option<u64>, |
| |
| /// Whether to compress the FVM. |
| #[serde(default)] |
| pub compress: bool, |
| |
| /// The number of blocks. |
| pub block_count: u64, |
| |
| /// The out of bound size. |
| pub oob_size: u64, |
| |
| /// Page size as perceived by the FTL. |
| pub page_size: u64, |
| |
| /// Number of pages per erase block unit. |
| pub pages_per_block: u64, |
| } |
| |
| impl ImagesConfig { |
| /// Parse the config from a reader. |
| pub fn from_reader<R>(reader: &mut R) -> Result<Self> |
| where |
| R: Read, |
| { |
| let mut data = String::default(); |
| reader.read_to_string(&mut data).context("Cannot read the config")?; |
| serde_json5::from_str(&data).context("Cannot parse the config") |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use std::convert::TryInto; |
| |
| #[test] |
| fn zbi_compression_try_from() { |
| assert_eq!(ZbiCompression::ZStd, "zstd".try_into().unwrap()); |
| assert_eq!(ZbiCompression::ZStdMax, "zstd.max".try_into().unwrap()); |
| let compression: Result<ZbiCompression> = "else".try_into(); |
| assert!(compression.is_err()); |
| } |
| |
| #[test] |
| fn zbi_compression_from_string() { |
| assert_eq!(ZbiCompression::ZStd, ZbiCompression::from_str("zstd").unwrap()); |
| assert_eq!(ZbiCompression::ZStdMax, ZbiCompression::from_str("zstd.max").unwrap()); |
| let compression: Result<ZbiCompression> = ZbiCompression::from_str("else"); |
| assert!(compression.is_err()); |
| } |
| |
| #[test] |
| fn zbi_compressoin_to_string() { |
| assert_eq!("zstd".to_string(), ZbiCompression::ZStd.to_string()); |
| assert_eq!("zstd.max".to_string(), ZbiCompression::ZStdMax.to_string()); |
| } |
| |
| #[test] |
| fn blobfs_layout_try_from() { |
| assert_eq!(BlobFSLayout::Compact, "compact".try_into().unwrap()); |
| assert_eq!(BlobFSLayout::DeprecatedPadded, "deprecated_padded".try_into().unwrap()); |
| let layout: Result<BlobFSLayout> = "else".try_into(); |
| assert!(layout.is_err()); |
| } |
| |
| #[test] |
| fn blobfs_layout_from_string() { |
| assert_eq!(BlobFSLayout::Compact, BlobFSLayout::from_str("compact").unwrap()); |
| assert_eq!( |
| BlobFSLayout::DeprecatedPadded, |
| BlobFSLayout::from_str("deprecated_padded").unwrap() |
| ); |
| let layout: Result<BlobFSLayout> = BlobFSLayout::from_str("else"); |
| assert!(layout.is_err()); |
| } |
| |
| #[test] |
| fn blobfs_layout_to_string() { |
| assert_eq!("compact".to_string(), BlobFSLayout::Compact.to_string()); |
| assert_eq!("deprecated_padded".to_string(), BlobFSLayout::DeprecatedPadded.to_string()); |
| } |
| |
| #[test] |
| fn from_json() { |
| let json = r#" |
| { |
| images: [ |
| { |
| type: "zbi", |
| name: "fuchsia", |
| compression: "zstd.max", |
| postprocessing_script: { |
| path: "path/to/tool.sh", |
| args: [ "arg1", "arg2" ] |
| } |
| }, |
| { |
| type: "vbmeta", |
| name: "fuchsia", |
| key: "path/to/key", |
| key_metadata: "path/to/key/metadata", |
| additional_descriptors: [ |
| { |
| name: "zircon", |
| size: 12345, |
| flags: 1, |
| min_avb_version: "1.1" |
| } |
| ] |
| }, |
| { |
| type: "fvm", |
| slice_size: 0, |
| filesystems: [ |
| { |
| type: "blobfs", |
| name: "blob", |
| layout: "compact", |
| maximum_bytes: 0, |
| minimum_data_bytes: 0, |
| minimum_inodes: 0, |
| }, |
| { |
| type: "minfs", |
| name: "data", |
| maximum_bytes: 0, |
| minimum_data_bytes: 0, |
| minimum_inodes: 0, |
| }, |
| { |
| type: "reserved", |
| name: "internal", |
| slices: 0, |
| }, |
| ], |
| outputs: [ |
| { |
| type: "standard", |
| name: "fvm", |
| filesystems: [ |
| "blob", |
| "data", |
| "internal", |
| ], |
| resize_image_file_to_fit: true, |
| truncate_to_length: 0, |
| }, |
| { |
| type: "sparse", |
| name: "fvm.sparse", |
| filesystems: [ |
| "blob", |
| "data", |
| "internal", |
| ], |
| max_disk_size: 0, |
| }, |
| { |
| type: "nand", |
| name: "fvm.nand", |
| filesystems: [ |
| "blob", |
| "data", |
| "internal", |
| ], |
| compress: true, |
| max_disk_size: 0, |
| block_count: 0, |
| oob_size: 0, |
| page_size: 0, |
| pages_per_block: 0, |
| }, |
| ], |
| }, |
| ], |
| } |
| "#; |
| let mut cursor = std::io::Cursor::new(json); |
| let config: ImagesConfig = ImagesConfig::from_reader(&mut cursor).unwrap(); |
| assert_eq!(config.images.len(), 3); |
| |
| let mut found_zbi = false; |
| let mut found_vbmeta = false; |
| let mut found_standard_fvm = false; |
| for image in config.images { |
| match image { |
| Image::Zbi(zbi) => { |
| found_zbi = true; |
| assert_eq!(zbi.name, "fuchsia"); |
| assert!(matches!(zbi.compression, ZbiCompression::ZStdMax)); |
| } |
| Image::VBMeta(vbmeta) => { |
| found_vbmeta = true; |
| assert_eq!(vbmeta.name, "fuchsia"); |
| assert_eq!(vbmeta.key, PathBuf::from("path/to/key")); |
| } |
| Image::Fvm(fvm) => { |
| assert_eq!(fvm.outputs.len(), 3); |
| |
| for output in fvm.outputs { |
| match output { |
| FvmOutput::Standard(standard) => { |
| found_standard_fvm = true; |
| assert_eq!(standard.name, "fvm"); |
| assert_eq!(standard.filesystems.len(), 3); |
| } |
| _ => {} |
| } |
| } |
| } |
| } |
| } |
| assert!(found_zbi); |
| assert!(found_vbmeta); |
| assert!(found_standard_fvm); |
| } |
| |
| #[test] |
| fn using_defaults() { |
| let json = r#" |
| { |
| images: [ |
| { |
| type: "zbi", |
| }, |
| { |
| type: "vbmeta", |
| key: "path/to/key", |
| key_metadata: "path/to/key/metadata", |
| }, |
| { |
| type: "fvm", |
| filesystems: [ |
| { |
| type: "blobfs", |
| name: "blob", |
| }, |
| { |
| type: "minfs", |
| name: "data", |
| }, |
| { |
| type: "reserved", |
| name: "internal", |
| slices: 0, |
| }, |
| ], |
| outputs: [ |
| { |
| type: "standard", |
| name: "fvm.blk", |
| filesystems: [ |
| "blob", |
| "data", |
| "internal", |
| ], |
| }, |
| { |
| type: "sparse", |
| name: "fvm.sparse.blk", |
| filesystems: [ |
| "blob", |
| "data", |
| "internal", |
| ], |
| }, |
| { |
| type: "nand", |
| name: "fvm.fastboot.blk", |
| filesystems: [ |
| "blob", |
| "data", |
| "internal", |
| ], |
| block_count: 0, |
| oob_size: 0, |
| page_size: 0, |
| pages_per_block: 0, |
| }, |
| ], |
| }, |
| ], |
| } |
| "#; |
| let mut cursor = std::io::Cursor::new(json); |
| let config: ImagesConfig = ImagesConfig::from_reader(&mut cursor).unwrap(); |
| assert_eq!(config.images.len(), 3); |
| } |
| } |