blob: 643a53053d76c38fabd231ec049245a873625095 [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 ::update_package::{ImageMetadata, ImagePackagesManifest};
use anyhow::{anyhow, bail, Context, Result};
use assembly_blob_size::BlobSizeCalculator;
use assembly_images_config::BlobfsLayout;
use assembly_manifest::{AssemblyManifest, Image};
use assembly_partitions_config::PartitionsConfig;
use assembly_tool::ToolProvider;
use assembly_update_packages_manifest::UpdatePackagesManifest;
use camino::{Utf8Path, Utf8PathBuf};
use epoch::EpochFile;
use fuchsia_merkle::Hash;
use fuchsia_pkg::{PackageBuilder, PackageManifest};
use fuchsia_url::{PinnedAbsolutePackageUrl, RepositoryUrl};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use utf8_path::PathToStringExt;
/// Maximum size of 200 KiB.
const UPDATE_PACKAGE_BUDGET: u64 = 200 * 1024 * 1024;
/// The result of the builder.
pub struct UpdatePackage {
/// The merkle-root of the update package.
pub merkle: Hash,
/// The package manifests corresponding to all the packages built for the update.
pub package_manifests: Vec<PackageManifest>,
}
/// A builder that constructs update packages.
pub struct UpdatePackageBuilder {
/// 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,
/// The repository to use for the images packages.
repository: RepositoryUrl,
/// Directory to write outputs.
outdir: Utf8PathBuf,
/// Directory to write intermediate files.
gendir: Utf8PathBuf,
}
/// A set of images to be updated in a particular slot.
pub enum Slot {
/// A or B slots.
Primary(AssemblyManifest),
/// R slot.
Recovery(AssemblyManifest),
}
impl Slot {
/// Get the image manifest.
fn manifest(&self) -> &AssemblyManifest {
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: Utf8PathBuf,
destination: String,
}
impl ImageMapping {
/// Create a new Image Mapping from |source | to |destination|.
fn new(source: impl Into<Utf8PathBuf>, destination: impl AsRef<str>) -> Self {
Self { source: source.into(), destination: destination.as_ref().to_string() }
}
fn metadata(&self, url: PinnedAbsolutePackageUrl) -> Result<ImageMetadata> {
ImageMetadata::for_path(&self.source, url, self.destination.clone())
.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,
far_path: Utf8PathBuf,
repository: RepositoryUrl,
gendir: Utf8PathBuf,
}
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) -> Result<(PinnedAbsolutePackageUrl, PackageManifest)> {
let SubpackageBuilder { package: builder, package_name, far_path, repository, gendir } =
self;
let manifest = builder
.build(&gendir, &far_path)
.with_context(|| format!("Failed to build the {package_name} package"))?;
let url = PinnedAbsolutePackageUrl::new(
repository,
manifest.package_path().name().clone(),
Some(manifest.package_path().variant().clone()),
manifest.hash(),
);
Ok((url, manifest))
}
}
impl UpdatePackageBuilder {
/// Construct a new UpdatePackageBuilder with the minimal requirements for an UpdatePackage.
pub fn new(
partitions: PartitionsConfig,
board_name: impl AsRef<str>,
version_file: impl AsRef<Path>,
epoch: EpochFile,
outdir: impl AsRef<Utf8Path>,
) -> Self {
Self {
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(),
repository: RepositoryUrl::parse_host("fuchsia.com".to_string())
.expect("valid host from static string"),
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 Into<Utf8PathBuf>) {
self.gendir = gendir.into();
}
/// 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);
}
/// 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}"),
};
// It's not totally clear what the ABI revision means for the update
// package. It isn't actually checked as part of the update process.
// Maybe it should be - that way we could ensure that devices only apply
// update packages they know they understand (currently, those checks
// happen at a different layer that predates ABI revisions).
//
// If the ABI stamp *was* checked as part of the update process, we'd
// have to be very deliberate about choosing which API level to target,
// based on which versions of the OS we need to be able to consume the
// update package.
//
// We'll set it to `INVALID` and decide on a more appropriate ABI
// revision if/when we decide to check it. Any checks on the `INVALID`
// ABI revision will fail, so this will hopefully ensure we don't
// accidentally add any checks without the necessary care.
//
// TODO(https://fxbug.dev/328812629): Clarify what this means.
let abi_revision = version_history::AbiRevision::INVALID;
// 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, abi_revision);
// 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,
far_path,
repository: self.repository.clone(),
gendir,
})
}
/// Set a custom repository to use when building the images packages.
pub fn set_repository(&mut self, repository: RepositoryUrl) {
self.repository = repository;
}
/// Build the update package and associated update images packages.
pub fn build(self, tools: Box<dyn ToolProvider>) -> Result<UpdatePackage> {
use serde_json::to_string;
// Keep track of all the packages that were built, so that they can be returned.
let mut package_manifests = vec![];
let mut assembly_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.to_string())?;
if let Some(vbmeta) = &vbmeta {
builder.package.add_file_as_blob(&vbmeta.destination, vbmeta.source.to_string())?;
}
let (url, manifest) = builder.build()?;
package_manifests.push(manifest);
assembly_manifest.fuchsia_package(
zbi.metadata(url.clone())?,
vbmeta.map(|vbmeta| vbmeta.metadata(url)).transpose()?,
);
} else {
let (_, manifest) = builder.build()?;
package_manifests.push(manifest);
}
// 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.to_string())?;
if let Some(vbmeta) = &vbmeta {
builder.package.add_file_as_blob(&vbmeta.destination, vbmeta.source.to_string())?;
}
let (url, manifest) = builder.build()?;
package_manifests.push(manifest);
assembly_manifest.recovery_package(
zbi.metadata(url.clone())?,
vbmeta.map(|vbmeta| vbmeta.metadata(url)).transpose()?,
);
} else {
let (_, manifest) = builder.build()?;
package_manifests.push(manifest);
}
// 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.to_string())?;
}
let (url, manifest) = builder.build()?;
package_manifests.push(manifest);
for bootloader in &self.partitions.bootloader_partitions {
let destination = match bootloader.partition_type.as_str() {
"" => "firmware".to_string(),
t => format!("firmware_{}", t),
};
firmware.insert(
bootloader.partition_type.clone(),
ImageMetadata::for_path(&bootloader.image, url.clone(), destination)
.with_context(|| format!("Failed to read/hash {:?}", &bootloader.image))?,
);
}
assembly_manifest.firmware_package(firmware);
} else {
let (_, manifest) = builder.build()?;
package_manifests.push(manifest);
}
let assembly_manifest = assembly_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(
"images.json",
to_string(&assembly_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()?)?;
let (_, manifest) = builder.build()?;
// Ensure the update package is within size budget.
// We use the worse layout for compression just in case.
let blob_size_calculator = BlobSizeCalculator::new(tools, BlobfsLayout::DeprecatedPadded);
let manifest_path = self.outdir.join("update_package_manifest.json");
let blobs = blob_size_calculator
.calculate(&vec![&manifest_path])
.context("Calculating update package blob sizes")?;
let mut total: u64 = 0;
for blob in blobs {
total += blob.size;
}
if total > UPDATE_PACKAGE_BUDGET {
bail!(
"Update package is over budget\nbudget: {}\nactual: {}",
UPDATE_PACKAGE_BUDGET,
total
);
}
let merkle = manifest.hash();
package_manifests.push(manifest);
Ok(UpdatePackage { merkle, package_manifests })
}
}
#[cfg(test)]
mod tests {
use super::*;
use assembly_partitions_config::Slot as PartitionSlot;
use assembly_partitions_config::{BootloaderPartition, Partition};
use assembly_tool::testing::{blobfs_side_effect, FakeToolProvider};
use assembly_util::write_json_file;
use fuchsia_archive::Utf8Reader;
use fuchsia_hash::HASH_SIZE;
use fuchsia_pkg::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 tmp = tempdir().unwrap();
let outdir = Utf8Path::from_path(tmp.path()).unwrap();
let fake_bootloader_tmp = NamedTempFile::new().unwrap();
let fake_bootloader = Utf8Path::from_path(fake_bootloader_tmp.path()).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.to_path_buf(),
}],
partitions: vec![Partition::ZBI {
name: "zircon_a".into(),
slot: PartitionSlot::A,
size: None,
}],
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(
partitions_config,
"board",
fake_version.path().to_path_buf(),
epoch.clone(),
&outdir,
);
// Add a ZBI to the update.
let fake_zbi_tmp = NamedTempFile::new().unwrap();
let fake_zbi = Utf8Path::from_path(fake_zbi_tmp.path()).unwrap();
builder.add_slot_images(Slot::Primary(AssemblyManifest {
images: vec![Image::ZBI { path: fake_zbi.to_path_buf(), signed: true }],
}));
builder.set_repository(RepositoryUrl::parse_host("test.com".to_string()).unwrap());
let tool_provider = Box::new(FakeToolProvider::new_with_side_effect(blobfs_side_effect));
builder.build(tool_provider).unwrap();
let file = File::open(outdir.join("images.json")).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": {
"partitions": [
{
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 0,
"slot": "fuchsia",
"type": "zbi",
"url": "fuchsia-pkg://test.com/update_images_fuchsia/0?hash=5e09a4766c1db520e8a871f8301e2046dc258bfb8eb1163c5f82d524d21d5c3f#zbi",
},
],
"firmware":
[{
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 0,
"url": "fuchsia-pkg://test.com/update_images_firmware/0?hash=97ae32f0e5edfb0688f1a8bee3cab63d3de82b6c9fdb888f050da8eab17a18b2#firmware_tpl",
"type": "tpl",
}],
},
}),
i
);
let file = File::open(outdir.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.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.join("board")).unwrap();
assert_eq!("board", b);
// Read the output and ensure it contains the right files (and their hashes).
let far_path = outdir.join("update.far");
let mut far_reader = Utf8Reader::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\
images.json=95a71ad6b533ac0a5b5ef4effb68f10098b60664d04ee6d4b06de870d1b9edf5\n\
packages.json=85a3911ff39c118ee1a4be5f7a117f58a5928a559f456b6874440a7fb8c47a9a\n\
version=d2ff44655653e2cbbecaf89dbf33a8daa8867e41dade2c6b4f127c3f0450c96b\n\
"
.to_string();
assert_eq!(expected_contents, contents);
let far_path = outdir.join("update_images_fuchsia.far");
let mut far_reader = Utf8Reader::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.join("update_images_recovery.far");
let mut far_reader = Utf8Reader::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.join("update_images_firmware.far");
let mut far_reader = Utf8Reader::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.join("update.far").exists());
assert!(outdir.join("update_package_manifest.json").exists());
assert!(outdir.join("update_images_fuchsia.far").exists());
assert!(outdir.join("update_images_recovery.far").exists());
assert!(outdir.join("update_images_firmware.far").exists());
assert!(outdir.join("update_images_fuchsia_package_manifest.json").exists());
assert!(outdir.join("update_images_recovery_package_manifest.json").exists());
assert!(outdir.join("update_images_firmware_package_manifest.json").exists());
}
#[test]
fn build_full() {
let tmp = tempdir().unwrap();
let outdir = Utf8Path::from_path(tmp.path()).unwrap();
let fake_bootloader_tmp = NamedTempFile::new().unwrap();
let fake_bootloader = Utf8Path::from_path(fake_bootloader_tmp.path()).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.to_path_buf(),
}],
partitions: vec![Partition::ZBI {
name: "zircon_a".into(),
slot: PartitionSlot::A,
size: None,
}],
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(
partitions_config,
"board",
fake_version.path().to_path_buf(),
epoch.clone(),
&outdir,
);
// Add a ZBI to the update.
let fake_zbi_tmp = NamedTempFile::new().unwrap();
let fake_zbi = Utf8Path::from_path(fake_zbi_tmp.path()).unwrap();
builder.add_slot_images(Slot::Primary(AssemblyManifest {
images: vec![Image::ZBI { path: fake_zbi.to_path_buf(), signed: true }],
}));
// Add a Recovery ZBI/VBMeta to the update.
let fake_recovery_zbi_tmp = NamedTempFile::new().unwrap();
let fake_recovery_zbi = Utf8Path::from_path(fake_recovery_zbi_tmp.path()).unwrap();
let fake_recovery_vbmeta_tmp = NamedTempFile::new().unwrap();
let fake_recovery_vbmeta = Utf8Path::from_path(fake_recovery_vbmeta_tmp.path()).unwrap();
builder.add_slot_images(Slot::Recovery(AssemblyManifest {
images: vec![
Image::ZBI { path: fake_recovery_zbi.to_path_buf(), signed: true },
Image::VBMeta(fake_recovery_vbmeta.to_path_buf()),
],
}));
// Build and ensure the output is correct.
builder.set_repository(RepositoryUrl::parse_host("test.com".to_string()).unwrap());
let tool_provider = Box::new(FakeToolProvider::new_with_side_effect(blobfs_side_effect));
let update_package = builder.build(tool_provider).unwrap();
assert_eq!(
update_package.merkle,
"128778df19f0f5d903c3d9ee291d367b8842830e05b90f48461c8005b815251a".parse().unwrap()
);
assert_eq!(update_package.package_manifests.len(), 4);
let file = File::open(outdir.join("images.json")).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": {
"partitions": [
{
"type": "zbi",
"slot": "fuchsia",
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 0,
"url": "fuchsia-pkg://test.com/update_images_fuchsia/0?hash=5e09a4766c1db520e8a871f8301e2046dc258bfb8eb1163c5f82d524d21d5c3f#zbi",
},
{
"type": "zbi",
"slot": "recovery",
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 0,
"url": "fuchsia-pkg://test.com/update_images_recovery/0?hash=0717335a7ccc00223d1f6b443cac36539dbf477f36abfbd4ac00623a6f587a47#zbi",
},
{
"type": "vbmeta",
"slot": "recovery",
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 0,
"url": "fuchsia-pkg://test.com/update_images_recovery/0?hash=0717335a7ccc00223d1f6b443cac36539dbf477f36abfbd4ac00623a6f587a47#vbmeta",
},
],
"firmware": [
{
"type" : "tpl",
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 0,
"url": "fuchsia-pkg://test.com/update_images_firmware/0?hash=97ae32f0e5edfb0688f1a8bee3cab63d3de82b6c9fdb888f050da8eab17a18b2#firmware_tpl",
},
],
},
}),
i
);
let file = File::open(outdir.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.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.join("board")).unwrap();
assert_eq!("board", b);
// Read the output and ensure it contains the right files (and their hashes).
let far_path = outdir.join("update.far");
let mut far_reader = Utf8Reader::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\
images.json=13893c40dd7b244cb289dddc2d06dea49c61462830e1b33765874dcff5581096\n\
packages.json=85a3911ff39c118ee1a4be5f7a117f58a5928a559f456b6874440a7fb8c47a9a\n\
version=d2ff44655653e2cbbecaf89dbf33a8daa8867e41dade2c6b4f127c3f0450c96b\n\
"
.to_string();
assert_eq!(expected_contents, contents);
let far_path = outdir.join("update_images_fuchsia.far");
let mut far_reader = Utf8Reader::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.join("update_images_recovery.far");
let mut far_reader = Utf8Reader::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.join("update_images_firmware.far");
let mut far_reader = Utf8Reader::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.join("update.far").exists());
assert!(outdir.join("update_package_manifest.json").exists());
assert!(outdir.join("update_images_fuchsia.far").exists());
assert!(outdir.join("update_images_recovery.far").exists());
assert!(outdir.join("update_images_firmware.far").exists());
assert!(outdir.join("update_images_fuchsia_package_manifest.json").exists());
assert!(outdir.join("update_images_recovery_package_manifest.json").exists());
assert!(outdir.join("update_images_firmware_package_manifest.json").exists());
}
#[test]
fn build_emits_empty_image_packages() {
let tmp = tempdir().unwrap();
let outdir = Utf8Path::from_path(tmp.path()).unwrap();
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(
partitions_config,
"board",
fake_version.path().to_path_buf(),
epoch.clone(),
&outdir,
);
let tool_provider = Box::new(FakeToolProvider::new_with_side_effect(blobfs_side_effect));
builder.build(tool_provider).unwrap();
// Ensure the generated images.json manifest is empty.
let file = File::open(outdir.join("images.json")).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.join("update.far").exists());
assert!(outdir.join("update_package_manifest.json").exists());
assert!(outdir.join("update_images_fuchsia.far").exists());
assert!(outdir.join("update_images_recovery.far").exists());
assert!(outdir.join("update_images_firmware.far").exists());
assert!(outdir.join("update_images_fuchsia_package_manifest.json").exists());
assert!(outdir.join("update_images_recovery_package_manifest.json").exists());
assert!(outdir.join("update_images_firmware_package_manifest.json").exists());
}
#[test]
fn name() {
let tmp = tempdir().unwrap();
let outdir = Utf8Path::from_path(tmp.path()).unwrap();
let mut fake_version = NamedTempFile::new().unwrap();
writeln!(fake_version, "1.2.3.4").unwrap();
let mut builder = UpdatePackageBuilder::new(
PartitionsConfig::default(),
"board",
fake_version.path().to_path_buf(),
EpochFile::Version1 { epoch: 0 },
&outdir,
);
builder.set_name("update_2");
let tool_provider = Box::new(FakeToolProvider::new_with_side_effect(blobfs_side_effect));
assert!(builder.build(tool_provider).is_ok());
// Read the package manifest and ensure it contains the updated name.
let manifest_path = outdir.join("update_package_manifest.json");
let manifest = PackageManifest::try_load_from(manifest_path).unwrap();
assert_eq!("update_2", manifest.name().as_ref());
}
#[test]
fn over_budget() {
let tmp = tempdir().unwrap();
let outdir = Utf8Path::from_path(tmp.path()).unwrap();
let mut fake_version = NamedTempFile::new().unwrap();
writeln!(fake_version, "1.2.3.4").unwrap();
let mut builder = UpdatePackageBuilder::new(
PartitionsConfig::default(),
"board",
fake_version.path().to_path_buf(),
EpochFile::Version1 { epoch: 0 },
&outdir,
);
builder.set_name("update_2");
let tool_provider =
Box::new(FakeToolProvider::new_with_side_effect(|_name: &str, args: &[String]| {
assert_eq!(args[0], "--json-output");
write_json_file(
Path::new(&args[1]),
&json!([{
"merkle": "b62ee413090825c2ae70fe143b34cbd851f055932cfd5e7ca4ef0efbb802da2a",
"size": UPDATE_PACKAGE_BUDGET + 1,
}]),
)
.unwrap();
}));
assert!(builder.build(tool_provider).is_err());
}
#[test]
fn packages() {
let tmp = tempdir().unwrap();
let outdir = Utf8Path::from_path(tmp.path()).unwrap();
let mut fake_version = NamedTempFile::new().unwrap();
writeln!(fake_version, "1.2.3.4").unwrap();
let mut builder = UpdatePackageBuilder::new(
PartitionsConfig::default(),
"board",
fake_version.path().to_path_buf(),
EpochFile::Version1 { epoch: 0 },
&outdir,
);
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);
let tool_provider = Box::new(FakeToolProvider::new_with_side_effect(blobfs_side_effect));
assert!(builder.build(tool_provider).is_ok());
// Read the package list and ensure it contains the correct contents.
let package_list_path = outdir.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));
}
}