blob: 48a97d48c8cd4575fbbdfc0837cad94f028df097 [file]
// 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 ::update_package::{ImageMetadata, ImagePackagesManifest};
use anyhow::{anyhow, Context, Result};
use assembly_blobfs::BlobFSBuilder;
use assembly_images_manifest::{Image, ImagesManifest};
use assembly_partitions_config::PartitionsConfig;
use assembly_tool::ToolProvider;
use assembly_update_packages_manifest::UpdatePackagesManifest;
use assembly_util::PathToStringExt;
use epoch::EpochFile;
use fuchsia_pkg::PackageBuilder;
use fuchsia_url::{PinnedAbsolutePackageUrl, RepositoryUrl};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
/// A builder that constructs update packages.
pub struct UpdatePackageBuilder {
/// The tool provider, used to invoke the blobfs tool from the SDK.
tool_provider: Box<dyn ToolProvider>,
/// Root name of the UpdatePackage and its associated images packages.
/// This is typically only modified for OTA tests so that multiple UpdatePackages can be
/// published to the same repository.
name: String,
/// Mapping of physical partitions to images.
partitions: PartitionsConfig,
/// Name of the board.
/// Fuchsia confirms the board name matches before applying an update.
board_name: String,
/// Version of the update.
version_file: PathBuf,
/// The epoch of the system.
/// Fuchsia confirms that the epoch changes in increasing order before applying an update.
epoch: EpochFile,
/// Images to update for a particular slot, such as the ZBI or VBMeta for SlotA.
/// Currently, the UpdatePackage does not support both A and B slots.
slot_primary: Option<Slot>,
slot_recovery: Option<Slot>,
/// Manifest of packages to include in the update.
packages: UpdatePackagesManifest,
/// Directory to write outputs.
outdir: PathBuf,
/// Directory to write intermediate files.
gendir: PathBuf,
}
/// A set of images to be updated in a particular slot.
pub enum Slot {
/// A or B slots.
Primary(ImagesManifest),
/// R slot.
Recovery(ImagesManifest),
}
impl Slot {
/// Get the image manifest.
fn manifest(&self) -> &ImagesManifest {
match self {
Slot::Primary(m) => m,
Slot::Recovery(m) => m,
}
}
/// Get the (preferably signed) zbi and optional vbmeta, or None if no zbi image is present in
/// this manifest.
fn zbi_and_vbmeta(&self) -> Option<(ImageMapping, Option<ImageMapping>)> {
let mut zbi = None;
let mut vbmeta = None;
for image in &self.manifest().images {
match image {
Image::ZBI { path: _, signed } => {
if *signed || zbi.is_none() {
zbi = Some(ImageMapping::new(image.source(), "zbi"));
}
}
Image::VBMeta(_) => {
vbmeta = Some(ImageMapping::new(image.source(), "vbmeta"));
}
_ => {}
}
}
match zbi {
Some(zbi) => Some((zbi, vbmeta)),
None => None,
}
}
}
/// A mapping between an image source path on host to the destination in an UpdatePackage.
struct ImageMapping {
source: PathBuf,
destination: String,
}
impl ImageMapping {
/// Create a new Image Mapping from |source | to |destination|.
fn new(source: impl AsRef<Path>, destination: impl AsRef<str>) -> Self {
Self {
source: source.as_ref().to_path_buf(),
destination: destination.as_ref().to_string(),
}
}
/// Create an ImageMapping from the |image| and |slot|.
fn try_from(image: &Image, slot: &Slot) -> Result<Self> {
match slot {
Slot::Primary(_) => match image {
Image::ZBI { path: _, signed: true } => {
Ok(ImageMapping::new(image.source(), "zbi.signed"))
}
Image::ZBI { path: _, signed: false } => {
Ok(ImageMapping::new(image.source(), "zbi"))
}
Image::VBMeta(_) => Ok(ImageMapping::new(image.source(), "fuchsia.vbmeta")),
_ => Err(anyhow!("Invalid primary image mapping")),
},
Slot::Recovery(_) => match image {
Image::ZBI { path: _, signed: _ } => {
Ok(ImageMapping::new(image.source(), "recovery"))
}
Image::VBMeta(_) => Ok(ImageMapping::new(image.source(), "recovery.vbmeta")),
_ => Err(anyhow!("Invalid recovery image mapping")),
},
}
}
fn metadata(&self) -> Result<ImageMetadata> {
ImageMetadata::for_path(&self.source)
.with_context(|| format!("Failed to read/hash {:?}", self.source))
}
}
/// A PackageBuilder configured to build the update package or one of its subpackages.
struct SubpackageBuilder {
package: PackageBuilder,
package_name: String,
manifest_path: PathBuf,
far_path: PathBuf,
gendir: PathBuf,
}
impl SubpackageBuilder {
/// Build and publish an update package or one of its subpackages. Returns a merkle-pinned
/// fuchsia-pkg:// URL for the package with the hostname set to "fuchsia.com".
fn build(self, blobfs_builder: &mut BlobFSBuilder) -> Result<PinnedAbsolutePackageUrl> {
let SubpackageBuilder { package: builder, package_name, manifest_path, far_path, gendir } =
self;
let manifest = builder
.build(&gendir, &far_path)
.with_context(|| format!("Failed to build the {package_name} package"))?;
blobfs_builder.add_package(&manifest_path).with_context(|| {
format!("Adding the {package_name} package to update packages blobfs")
})?;
let url = PinnedAbsolutePackageUrl::new(
RepositoryUrl::parse_host("fuchsia.com".to_string())
.expect("valid host from static string"),
manifest.package_path().name().clone(),
Some(manifest.package_path().variant().clone()),
manifest.hash(),
);
Ok(url)
}
}
impl UpdatePackageBuilder {
/// Construct a new UpdatePackageBuilder with the minimal requirements for an UpdatePackage.
pub fn new(
tool_provider: Box<dyn ToolProvider>,
partitions: PartitionsConfig,
board_name: impl AsRef<str>,
version_file: impl AsRef<Path>,
epoch: EpochFile,
outdir: impl AsRef<Path>,
) -> Self {
Self {
tool_provider,
name: "update".into(),
partitions,
board_name: board_name.as_ref().into(),
version_file: version_file.as_ref().to_path_buf(),
epoch,
slot_primary: None,
slot_recovery: None,
packages: UpdatePackagesManifest::default(),
outdir: outdir.as_ref().to_path_buf(),
gendir: outdir.as_ref().to_path_buf(),
}
}
/// Set the name of the UpdatePackage.
pub fn set_name(&mut self, name: impl AsRef<str>) {
self.name = name.as_ref().to_string();
}
/// Set the directory for writing intermediate files.
pub fn set_gendir(&mut self, gendir: impl AsRef<Path>) {
self.gendir = gendir.as_ref().to_path_buf();
}
/// Update the images in |slot|.
pub fn add_slot_images(&mut self, slot: Slot) {
match slot {
Slot::Primary(_) => self.slot_primary = Some(slot),
Slot::Recovery(_) => self.slot_recovery = Some(slot),
}
}
/// Add |packages| to the update.
pub fn add_packages(&mut self, packages: UpdatePackagesManifest) {
self.packages.append(packages);
}
/// Add the ZBI and VBMeta from the |slot| to the |map|.
fn add_images_to_builder(slot: &Slot, builder: &mut PackageBuilder) -> Result<()> {
let mappings: Vec<ImageMapping> = slot
.manifest()
.images
.iter()
.filter_map(|i| ImageMapping::try_from(i, slot).ok())
.collect();
for ImageMapping { source, destination } in mappings {
builder.add_file_as_blob(destination, source.path_to_string()?)?;
}
Ok(())
}
/// Start building an update package or one of its subpackages, performing the steps that
/// are common to all update packages.
fn make_subpackage_builder(&self, subname: &str) -> Result<SubpackageBuilder> {
let suffix = match subname {
"" => subname.to_owned(),
_ => format!("_{subname}"),
};
// The update package needs to be named 'update' to be accepted by the
// `system-updater`. Follow that convention for images packages as well.
let package_name = format!("update{suffix}");
let mut builder = PackageBuilder::new(&package_name);
// However, they can have different published names. And the name here
// is the name to publish it under (and to include in the generated
// package manifest).
let base_publish_name = &self.name;
let publish_name = format!("{base_publish_name}{suffix}");
builder.published_name(publish_name);
// Export the package's package manifest to paths that don't change
// based on the configured publishing name.
let manifest_path = self.outdir.join(format!("update{suffix}_package_manifest.json"));
builder.manifest_path(&manifest_path);
let far_path = self.outdir.join(format!("{package_name}.far"));
let gendir = self.gendir.join(&package_name);
Ok(SubpackageBuilder { package: builder, package_name, manifest_path, far_path, gendir })
}
/// Build the update package and associated update images packages.
pub fn build(self) -> Result<()> {
use serde_json::to_string;
let blobfs_tool = self.tool_provider.get_tool("blobfs")?;
let mut blobfs_builder = BlobFSBuilder::new(blobfs_tool, "compact");
let mut images_manifest = ImagePackagesManifest::builder();
// Generate the update_images_fuchsia package.
let mut builder = self.make_subpackage_builder("images_fuchsia")?;
if let Some(slot) = &self.slot_primary {
let (zbi, vbmeta) =
slot.zbi_and_vbmeta().ok_or(anyhow!("primary slot missing a zbi image"))?;
builder.package.add_file_as_blob(&zbi.destination, zbi.source.path_to_string()?)?;
if let Some(vbmeta) = &vbmeta {
builder
.package
.add_file_as_blob(&vbmeta.destination, vbmeta.source.path_to_string()?)?;
}
let url = builder.build(&mut blobfs_builder)?;
images_manifest.fuchsia_package(
url,
zbi.metadata()?,
vbmeta.map(|vbmeta| vbmeta.metadata()).transpose()?,
);
} else {
builder.build(&mut blobfs_builder)?;
}
// Generate the update_images_recovery package.
let mut builder = self.make_subpackage_builder("images_recovery")?;
if let Some(slot) = &self.slot_recovery {
let (zbi, vbmeta) =
slot.zbi_and_vbmeta().ok_or(anyhow!("recovery slot missing a zbi image"))?;
builder.package.add_file_as_blob(&zbi.destination, zbi.source.path_to_string()?)?;
if let Some(vbmeta) = &vbmeta {
builder
.package
.add_file_as_blob(&vbmeta.destination, vbmeta.source.path_to_string()?)?;
}
let url = builder.build(&mut blobfs_builder)?;
images_manifest.recovery_package(
url,
zbi.metadata()?,
vbmeta.map(|vbmeta| vbmeta.metadata()).transpose()?,
);
} else {
builder.build(&mut blobfs_builder)?;
}
// Generate the update_images_firmware package.
let mut builder = self.make_subpackage_builder("images_firmware")?;
if !self.partitions.bootloader_partitions.is_empty() {
let mut firmware = BTreeMap::new();
for bootloader in &self.partitions.bootloader_partitions {
let destination = match bootloader.partition_type.as_str() {
"" => "firmware".to_string(),
t => format!("firmware_{}", t),
};
builder
.package
.add_file_as_blob(destination, bootloader.image.path_to_string()?)?;
firmware.insert(
bootloader.partition_type.clone(),
ImageMetadata::for_path(&bootloader.image)
.with_context(|| format!("Failed to read/hash {:?}", &bootloader.image))?,
);
}
let url = builder.build(&mut blobfs_builder)?;
images_manifest.firmware_package(url, firmware);
} else {
builder.build(&mut blobfs_builder)?;
}
let images_manifest = images_manifest.build();
// Generate the update package itself.
let mut builder = self.make_subpackage_builder("")?;
builder.package.add_contents_as_blob(
"packages.json",
to_string(&self.packages)?,
&self.gendir,
)?;
builder.package.add_contents_as_blob(
// Emit images.json as images.json.orig so the system-updater can differentiate
// between an images.json that hasn't been modified by downstream tooling and one
// that has. Once that tooling is modified to also modify/rename this manifest,
// this can be updated to write to images.json directly.
"images.json.orig",
to_string(&images_manifest)?,
&self.gendir,
)?;
builder.package.add_contents_as_blob(
"epoch.json",
to_string(&self.epoch)?,
&self.gendir,
)?;
builder.package.add_contents_as_blob("board", &self.board_name, &self.gendir)?;
builder.package.add_file_as_blob("version", self.version_file.path_to_string()?)?;
// Add the images.
let slots = vec![&self.slot_primary, &self.slot_recovery];
for slot in slots.iter().filter_map(|s| s.as_ref()) {
Self::add_images_to_builder(slot, &mut builder.package)?;
}
// Add the bootloaders.
for bootloader in &self.partitions.bootloader_partitions {
let destination = match bootloader.partition_type.as_str() {
"" => "firmware".to_string(),
t => format!("firmware_{}", t),
};
builder.package.add_file_as_blob(destination, bootloader.image.path_to_string()?)?;
}
builder.build(&mut blobfs_builder)?;
// Generate a blobfs with the generated update package and update images packages inside
// it. This is useful for packaging up the blobs to use in adversarial tests. We do not
// care which blobfs layout we use, or whether it is compressed, because blobfs is mostly
// just being used as a content-addressed container.
let update_blob_path = self.gendir.join("update.blob.blk");
blobfs_builder
.build(self.gendir, update_blob_path)
.context("Building blobfs for update package")?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use assembly_images_manifest::Image;
use assembly_partitions_config::Slot as PartitionSlot;
use assembly_partitions_config::{BootloaderPartition, Partition, PartitionsConfig};
use assembly_tool::testing::FakeToolProvider;
use assembly_tool::{ToolCommandLog, ToolProvider};
use assembly_update_packages_manifest::UpdatePackagesManifest;
use fuchsia_archive::Reader;
use fuchsia_hash::{Hash, HASH_SIZE};
use fuchsia_pkg::{PackageManifest, PackagePath};
use serde_json::json;
use std::fs::File;
use std::io::{BufReader, Write};
use std::str::FromStr;
use tempfile::{tempdir, NamedTempFile};
#[test]
fn build() {
let outdir = tempdir().unwrap();
let tools = FakeToolProvider::default();
let fake_bootloader = NamedTempFile::new().unwrap();
let partitions_config = PartitionsConfig {
bootstrap_partitions: vec![],
unlock_credentials: vec![],
bootloader_partitions: vec![BootloaderPartition {
partition_type: "tpl".into(),
name: Some("firmware_tpl".into()),
image: fake_bootloader.path().to_path_buf(),
}],
partitions: vec![Partition::ZBI { name: "zircon_a".into(), slot: PartitionSlot::A }],
hardware_revision: "hw".into(),
};
let epoch = EpochFile::Version1 { epoch: 0 };
let mut fake_version = NamedTempFile::new().unwrap();
writeln!(fake_version, "1.2.3.4").unwrap();
let mut builder = UpdatePackageBuilder::new(
Box::new(tools.clone()),
partitions_config,
"board",
fake_version.path().to_path_buf(),
epoch.clone(),
&outdir.path(),
);
// Add a ZBI to the update.
let fake_zbi = NamedTempFile::new().unwrap();
builder.add_slot_images(Slot::Primary(ImagesManifest {
images: vec![Image::ZBI { path: fake_zbi.path().to_path_buf(), signed: true }],
}));
builder.build().unwrap();
// Ensure the blobfs tool was invoked correctly.
let blob_blk_path = outdir.path().join("update.blob.blk").path_to_string().unwrap();
let blobs_json_path = outdir.path().join("blobs.json").path_to_string().unwrap();
let blob_manifest_path = outdir.path().join("blob.manifest").path_to_string().unwrap();
let expected_commands: ToolCommandLog = serde_json::from_value(json!({
"commands": [
{
"tool": "./host_x64/blobfs",
"args": [
"--json-output",
blobs_json_path,
blob_blk_path,
"create",
"--manifest",
blob_manifest_path,
]
}
]
}))
.unwrap();
assert_eq!(&expected_commands, tools.log());
let file = File::open(outdir.path().join("images.json.orig")).unwrap();
let reader = BufReader::new(file);
let i: serde_json::Value = serde_json::from_reader(reader).unwrap();
assert_eq!(
serde_json::json!({
"version": "1",
"contents": {
"fuchsia": {
"url": "fuchsia-pkg://fuchsia.com/update_images_fuchsia/0?hash=f9e2033364bdbb0e28d07882c8811a6219d266b7f5e1a7810b424f878ea19b30",
"images": {
"zbi": {
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 0,
},
},
},
"firmware": {
"url": "fuchsia-pkg://fuchsia.com/update_images_firmware/0?hash=795b05ab882f4f5959b50fcddeea18dba67b5ec6309c761460f0777e73c3f12d",
"images": {
"tpl": {
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 0,
},
},
},
},
}),
i
);
let file = File::open(outdir.path().join("packages.json")).unwrap();
let reader = BufReader::new(file);
let p: UpdatePackagesManifest = serde_json::from_reader(reader).unwrap();
assert_eq!(UpdatePackagesManifest::default(), p);
let file = File::open(outdir.path().join("epoch.json")).unwrap();
let reader = BufReader::new(file);
let e: EpochFile = serde_json::from_reader(reader).unwrap();
assert_eq!(epoch, e);
let b = std::fs::read_to_string(outdir.path().join("board")).unwrap();
assert_eq!("board", b);
// Read the output and ensure it contains the right files (and their hashes).
let far_path = outdir.path().join("update.far");
let mut far_reader = Reader::new(File::open(&far_path).unwrap()).unwrap();
let package = far_reader.read_file("meta/package").unwrap();
assert_eq!(package, br#"{"name":"update","version":"0"}"#);
let contents = far_reader.read_file("meta/contents").unwrap();
let contents = std::str::from_utf8(&contents).unwrap();
let expected_contents = "\
board=9c579992f6e9f8cbd4ba81af6e23b1d5741e280af60f795e9c2bbcc76c4b7065\n\
epoch.json=0362de83c084397826800778a1cf927280a5d5388cb1f828d77f74108726ad69\n\
firmware_tpl=15ec7bf0b50732b49f8228e07d24365338f9e3ab994b00af08e5a3bffe55fd8b\n\
images.json.orig=73894a190e5658901049dc9f83087309669b687ee5d6f40fd63de20e0196e76a\n\
packages.json=85a3911ff39c118ee1a4be5f7a117f58a5928a559f456b6874440a7fb8c47a9a\n\
version=d2ff44655653e2cbbecaf89dbf33a8daa8867e41dade2c6b4f127c3f0450c96b\n\
zbi.signed=15ec7bf0b50732b49f8228e07d24365338f9e3ab994b00af08e5a3bffe55fd8b\n\
"
.to_string();
assert_eq!(expected_contents, contents);
let far_path = outdir.path().join("update_images_fuchsia.far");
let mut far_reader = Reader::new(File::open(&far_path).unwrap()).unwrap();
let package = far_reader.read_file("meta/package").unwrap();
assert_eq!(package, br#"{"name":"update_images_fuchsia","version":"0"}"#);
let contents = far_reader.read_file("meta/contents").unwrap();
let contents = std::str::from_utf8(&contents).unwrap();
let expected_contents = "\
zbi=15ec7bf0b50732b49f8228e07d24365338f9e3ab994b00af08e5a3bffe55fd8b\n\
"
.to_string();
assert_eq!(expected_contents, contents);
let far_path = outdir.path().join("update_images_recovery.far");
let mut far_reader = Reader::new(File::open(&far_path).unwrap()).unwrap();
let package = far_reader.read_file("meta/package").unwrap();
assert_eq!(package, br#"{"name":"update_images_recovery","version":"0"}"#);
let contents = far_reader.read_file("meta/contents").unwrap();
let contents = std::str::from_utf8(&contents).unwrap();
let expected_contents = "\
"
.to_string();
assert_eq!(expected_contents, contents);
let far_path = outdir.path().join("update_images_firmware.far");
let mut far_reader = Reader::new(File::open(&far_path).unwrap()).unwrap();
let package = far_reader.read_file("meta/package").unwrap();
assert_eq!(package, br#"{"name":"update_images_firmware","version":"0"}"#);
let contents = far_reader.read_file("meta/contents").unwrap();
let contents = std::str::from_utf8(&contents).unwrap();
let expected_contents = "\
firmware_tpl=15ec7bf0b50732b49f8228e07d24365338f9e3ab994b00af08e5a3bffe55fd8b\n\
"
.to_string();
assert_eq!(expected_contents, contents);
// Ensure the expected package fars/manifests were generated.
assert!(outdir.path().join("update.far").exists());
assert!(outdir.path().join("update_package_manifest.json").exists());
assert!(outdir.path().join("update_images_fuchsia.far").exists());
assert!(outdir.path().join("update_images_recovery.far").exists());
assert!(outdir.path().join("update_images_firmware.far").exists());
assert!(outdir.path().join("update_images_fuchsia_package_manifest.json").exists());
assert!(outdir.path().join("update_images_recovery_package_manifest.json").exists());
assert!(outdir.path().join("update_images_firmware_package_manifest.json").exists());
}
#[test]
fn build_full() {
let outdir = tempdir().unwrap();
let tools = FakeToolProvider::default();
let fake_bootloader = NamedTempFile::new().unwrap();
let partitions_config = PartitionsConfig {
bootstrap_partitions: vec![],
unlock_credentials: vec![],
bootloader_partitions: vec![BootloaderPartition {
partition_type: "tpl".into(),
name: Some("firmware_tpl".into()),
image: fake_bootloader.path().to_path_buf(),
}],
partitions: vec![Partition::ZBI { name: "zircon_a".into(), slot: PartitionSlot::A }],
hardware_revision: "hw".into(),
};
let epoch = EpochFile::Version1 { epoch: 0 };
let mut fake_version = NamedTempFile::new().unwrap();
writeln!(fake_version, "1.2.3.4").unwrap();
let mut builder = UpdatePackageBuilder::new(
Box::new(tools.clone()),
partitions_config,
"board",
fake_version.path().to_path_buf(),
epoch.clone(),
&outdir.path(),
);
// Add a ZBI to the update.
let fake_zbi = NamedTempFile::new().unwrap();
builder.add_slot_images(Slot::Primary(ImagesManifest {
images: vec![Image::ZBI { path: fake_zbi.path().to_path_buf(), signed: true }],
}));
// Add a Recovery ZBI/VBMeta to the update.
let fake_recovery_zbi = NamedTempFile::new().unwrap();
let fake_recovery_vbmeta = NamedTempFile::new().unwrap();
builder.add_slot_images(Slot::Recovery(ImagesManifest {
images: vec![
Image::ZBI { path: fake_recovery_zbi.path().to_path_buf(), signed: true },
Image::VBMeta(fake_recovery_vbmeta.path().to_path_buf()),
],
}));
builder.build().unwrap();
// Ensure the blobfs tool was invoked correctly.
let blob_blk_path = outdir.path().join("update.blob.blk").path_to_string().unwrap();
let blobs_json_path = outdir.path().join("blobs.json").path_to_string().unwrap();
let blob_manifest_path = outdir.path().join("blob.manifest").path_to_string().unwrap();
let expected_commands: ToolCommandLog = serde_json::from_value(json!({
"commands": [
{
"tool": "./host_x64/blobfs",
"args": [
"--json-output",
blobs_json_path,
blob_blk_path,
"create",
"--manifest",
blob_manifest_path,
]
}
]
}))
.unwrap();
assert_eq!(&expected_commands, tools.log());
let file = File::open(outdir.path().join("images.json.orig")).unwrap();
let reader = BufReader::new(file);
let i: serde_json::Value = serde_json::from_reader(reader).unwrap();
assert_eq!(
serde_json::json!({
"version": "1",
"contents": {
"fuchsia": {
"url": "fuchsia-pkg://fuchsia.com/update_images_fuchsia/0?hash=f9e2033364bdbb0e28d07882c8811a6219d266b7f5e1a7810b424f878ea19b30",
"images": {
"zbi": {
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 0,
},
},
},
"recovery": {
"url": "fuchsia-pkg://fuchsia.com/update_images_recovery/0?hash=c0be02242469c633ddcaeaef1493b67158688f10e41131c7e19fbd2b5bc86acd",
"images": {
"vbmeta": {
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 0,
},
"zbi": {
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 0,
},
},
},
"firmware": {
"url": "fuchsia-pkg://fuchsia.com/update_images_firmware/0?hash=795b05ab882f4f5959b50fcddeea18dba67b5ec6309c761460f0777e73c3f12d",
"images": {
"tpl": {
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 0,
},
},
},
},
}),
i
);
let file = File::open(outdir.path().join("packages.json")).unwrap();
let reader = BufReader::new(file);
let p: UpdatePackagesManifest = serde_json::from_reader(reader).unwrap();
assert_eq!(UpdatePackagesManifest::default(), p);
let file = File::open(outdir.path().join("epoch.json")).unwrap();
let reader = BufReader::new(file);
let e: EpochFile = serde_json::from_reader(reader).unwrap();
assert_eq!(epoch, e);
let b = std::fs::read_to_string(outdir.path().join("board")).unwrap();
assert_eq!("board", b);
// Read the output and ensure it contains the right files (and their hashes).
let far_path = outdir.path().join("update.far");
let mut far_reader = Reader::new(File::open(&far_path).unwrap()).unwrap();
let package = far_reader.read_file("meta/package").unwrap();
assert_eq!(package, br#"{"name":"update","version":"0"}"#);
let contents = far_reader.read_file("meta/contents").unwrap();
let contents = std::str::from_utf8(&contents).unwrap();
let expected_contents = "\
board=9c579992f6e9f8cbd4ba81af6e23b1d5741e280af60f795e9c2bbcc76c4b7065\n\
epoch.json=0362de83c084397826800778a1cf927280a5d5388cb1f828d77f74108726ad69\n\
firmware_tpl=15ec7bf0b50732b49f8228e07d24365338f9e3ab994b00af08e5a3bffe55fd8b\n\
images.json.orig=9ad58f6f9d561c019c85fa616be925828a0d02b5c1805d02cebdbe47a4d5dbad\n\
packages.json=85a3911ff39c118ee1a4be5f7a117f58a5928a559f456b6874440a7fb8c47a9a\n\
recovery=15ec7bf0b50732b49f8228e07d24365338f9e3ab994b00af08e5a3bffe55fd8b\n\
recovery.vbmeta=15ec7bf0b50732b49f8228e07d24365338f9e3ab994b00af08e5a3bffe55fd8b\n\
version=d2ff44655653e2cbbecaf89dbf33a8daa8867e41dade2c6b4f127c3f0450c96b\n\
zbi.signed=15ec7bf0b50732b49f8228e07d24365338f9e3ab994b00af08e5a3bffe55fd8b\n\
"
.to_string();
assert_eq!(expected_contents, contents);
let far_path = outdir.path().join("update_images_fuchsia.far");
let mut far_reader = Reader::new(File::open(&far_path).unwrap()).unwrap();
let package = far_reader.read_file("meta/package").unwrap();
assert_eq!(package, br#"{"name":"update_images_fuchsia","version":"0"}"#);
let contents = far_reader.read_file("meta/contents").unwrap();
let contents = std::str::from_utf8(&contents).unwrap();
let expected_contents = "\
zbi=15ec7bf0b50732b49f8228e07d24365338f9e3ab994b00af08e5a3bffe55fd8b\n\
"
.to_string();
assert_eq!(expected_contents, contents);
let far_path = outdir.path().join("update_images_recovery.far");
let mut far_reader = Reader::new(File::open(&far_path).unwrap()).unwrap();
let package = far_reader.read_file("meta/package").unwrap();
assert_eq!(package, br#"{"name":"update_images_recovery","version":"0"}"#);
let contents = far_reader.read_file("meta/contents").unwrap();
let contents = std::str::from_utf8(&contents).unwrap();
let expected_contents = "\
vbmeta=15ec7bf0b50732b49f8228e07d24365338f9e3ab994b00af08e5a3bffe55fd8b\n\
zbi=15ec7bf0b50732b49f8228e07d24365338f9e3ab994b00af08e5a3bffe55fd8b\n\
"
.to_string();
assert_eq!(expected_contents, contents);
let far_path = outdir.path().join("update_images_firmware.far");
let mut far_reader = Reader::new(File::open(&far_path).unwrap()).unwrap();
let package = far_reader.read_file("meta/package").unwrap();
assert_eq!(package, br#"{"name":"update_images_firmware","version":"0"}"#);
let contents = far_reader.read_file("meta/contents").unwrap();
let contents = std::str::from_utf8(&contents).unwrap();
let expected_contents = "\
firmware_tpl=15ec7bf0b50732b49f8228e07d24365338f9e3ab994b00af08e5a3bffe55fd8b\n\
"
.to_string();
assert_eq!(expected_contents, contents);
// Ensure the expected package fars/manifests were generated.
assert!(outdir.path().join("update.far").exists());
assert!(outdir.path().join("update_package_manifest.json").exists());
assert!(outdir.path().join("update_images_fuchsia.far").exists());
assert!(outdir.path().join("update_images_recovery.far").exists());
assert!(outdir.path().join("update_images_firmware.far").exists());
assert!(outdir.path().join("update_images_fuchsia_package_manifest.json").exists());
assert!(outdir.path().join("update_images_recovery_package_manifest.json").exists());
assert!(outdir.path().join("update_images_firmware_package_manifest.json").exists());
}
#[test]
fn build_emits_empty_image_packages() {
let outdir = tempdir().unwrap();
let tools = FakeToolProvider::default();
let partitions_config = PartitionsConfig::default();
let epoch = EpochFile::Version1 { epoch: 0 };
let mut fake_version = NamedTempFile::new().unwrap();
writeln!(fake_version, "1.2.3.4").unwrap();
let builder = UpdatePackageBuilder::new(
Box::new(tools.clone()),
partitions_config,
"board",
fake_version.path().to_path_buf(),
epoch.clone(),
&outdir.path(),
);
builder.build().unwrap();
// Ensure the generated images.json manifest is empty.
let file = File::open(outdir.path().join("images.json.orig")).unwrap();
let reader = BufReader::new(file);
let i: ::update_package::VersionedImagePackagesManifest =
serde_json::from_reader(reader).unwrap();
assert_eq!(ImagePackagesManifest::builder().build(), i);
// Ensure the expected package fars/manifests were generated.
assert!(outdir.path().join("update.far").exists());
assert!(outdir.path().join("update_package_manifest.json").exists());
assert!(outdir.path().join("update_images_fuchsia.far").exists());
assert!(outdir.path().join("update_images_recovery.far").exists());
assert!(outdir.path().join("update_images_firmware.far").exists());
assert!(outdir.path().join("update_images_fuchsia_package_manifest.json").exists());
assert!(outdir.path().join("update_images_recovery_package_manifest.json").exists());
assert!(outdir.path().join("update_images_firmware_package_manifest.json").exists());
}
#[test]
fn name() {
let outdir = tempdir().unwrap();
let tools = FakeToolProvider::default();
let mut fake_version = NamedTempFile::new().unwrap();
writeln!(fake_version, "1.2.3.4").unwrap();
let mut builder = UpdatePackageBuilder::new(
Box::new(tools.clone()),
PartitionsConfig::default(),
"board",
fake_version.path().to_path_buf(),
EpochFile::Version1 { epoch: 0 },
&outdir.path(),
);
builder.set_name("update_2");
assert!(builder.build().is_ok());
// Read the package manifest and ensure it contains the updated name.
let manifest_path = outdir.path().join("update_package_manifest.json");
let manifest = PackageManifest::try_load_from(manifest_path).unwrap();
assert_eq!("update_2", manifest.name().as_ref());
}
#[test]
fn packages() {
let outdir = tempdir().unwrap();
let tools = FakeToolProvider::default();
let mut fake_version = NamedTempFile::new().unwrap();
writeln!(fake_version, "1.2.3.4").unwrap();
let mut builder = UpdatePackageBuilder::new(
Box::new(tools.clone()),
PartitionsConfig::default(),
"board",
fake_version.path().to_path_buf(),
EpochFile::Version1 { epoch: 0 },
&outdir.path(),
);
let hash = Hash::from([0u8; HASH_SIZE]);
let mut list1 = UpdatePackagesManifest::default();
list1.add(PackagePath::from_str("one/0").unwrap(), hash.clone(), None).unwrap();
list1.add(PackagePath::from_str("two/0").unwrap(), hash.clone(), None).unwrap();
builder.add_packages(list1);
let mut list2 = UpdatePackagesManifest::default();
list2.add(PackagePath::from_str("three/0").unwrap(), hash.clone(), None).unwrap();
list2.add(PackagePath::from_str("four/0").unwrap(), hash.clone(), None).unwrap();
builder.add_packages(list2);
assert!(builder.build().is_ok());
// Read the package list and ensure it contains the correct contents.
let package_list_path = outdir.path().join("packages.json");
let package_list_file = File::open(package_list_path).unwrap();
let list3: UpdatePackagesManifest = serde_json::from_reader(package_list_file).unwrap();
let UpdatePackagesManifest::V1(pkg_urls) = list3;
let pkg1 = PinnedAbsolutePackageUrl::new(
"fuchsia-pkg://fuchsia.com".parse().unwrap(),
"one".parse().unwrap(),
Some(fuchsia_url::PackageVariant::zero()),
hash.clone(),
);
println!("pkg_urls={:?}", &pkg_urls);
println!("pkg={:?}", pkg1);
assert!(pkg_urls.contains(&pkg1));
}
}