blob: 30348f28c82344ff12342a50a9951ff076859b0c [file] [log] [blame]
// Copyright 2020 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 {
fidl_fuchsia_io::DirectoryProxy,
fidl_fuchsia_mem::Buffer,
fuchsia_zircon::{Status, VmoChildOptions},
thiserror::Error,
};
/// An error encountered while opening an image.
#[derive(Debug, Error)]
#[allow(missing_docs)]
pub enum OpenImageError {
#[error("while opening the file")]
OpenFile(#[source] io_util::node::OpenError),
#[error("while calling get_buffer")]
FidlGetBuffer(#[source] fidl::Error),
#[error("while obtaining vmo of file: {0}")]
GetBuffer(fuchsia_zircon::Status),
#[error("remote reported success without providing a vmo")]
MissingBuffer,
#[error("while converting vmo to a resizable vmo: {0}")]
CloneBuffer(fuchsia_zircon::Status),
}
/// An identifier for an image type which corresponds to the file's name without
/// a subtype.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
pub enum ImageType {
/// Kernel image.
Zbi,
/// Kernel image.
ZbiSigned,
/// Metadata for the kernel image.
FuchsiaVbmeta,
/// Recovery image.
Zedboot,
/// Recovery image.
ZedbootSigned,
/// Recovery image.
Recovery,
/// Metadata for recovery image.
RecoveryVbmeta,
/// Bootloader.
Bootloader,
/// Firmware
Firmware,
}
impl ImageType {
/// The name of the ImageType.
pub fn name(&self) -> &'static str {
match self {
Self::Zbi => "zbi",
Self::ZbiSigned => "zbi.signed",
Self::FuchsiaVbmeta => "fuchsia.vbmeta",
Self::Zedboot => "zedboot",
Self::ZedbootSigned => "zedboot.signed",
Self::Recovery => "recovery",
Self::RecoveryVbmeta => "recovery.vbmeta",
Self::Bootloader => "bootloader",
Self::Firmware => "firmware",
}
}
}
/// An identifier for an image that can be paved.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Image {
imagetype: ImageType,
filename: String,
}
impl Image {
/// Construct an Image using the given imagetype and optional subtype.
pub fn new(imagetype: ImageType, subtype: Option<&str>) -> Self {
let filename = match subtype {
None => imagetype.name().to_string(),
Some(subtype) => format!("{}_{}", imagetype.name(), subtype),
};
Self { imagetype, filename }
}
/// The imagetype of the image relative to the update package.
pub fn imagetype(&self) -> ImageType {
self.imagetype
}
/// The name of this image as understood by the system updater.
pub fn classify(&self) -> ImageClass {
match self.imagetype() {
ImageType::Zbi | ImageType::ZbiSigned => ImageClass::Zbi,
ImageType::FuchsiaVbmeta => ImageClass::ZbiVbmeta,
ImageType::Zedboot | ImageType::ZedbootSigned | ImageType::Recovery => {
ImageClass::Recovery
}
ImageType::RecoveryVbmeta => ImageClass::RecoveryVbmeta,
ImageType::Bootloader | ImageType::Firmware => ImageClass::Firmware,
}
}
/// Attempt to construct an Image using the given `filename` if it is of the form "{name}" or
/// "{name}_{subtype}" where name is equal to the given `imagetype.name()`, or return None if
/// `filename` does not start with `imagetype.name()`.
///
/// # Examples
///
/// `firmware_bar` starts with `firmware`, so it matches:
/// ```
/// let image = Image::matches_base("firmware_bar", ImageType::Firmware).unwrap();
/// assert_eq!(image.subtype(), Some("bar"));
/// ```
///
/// `firmware_zbi` doesn't start with `zbi`, so it doesn't match:
/// ```
/// let image = Image::matches_base("firmware_zbi", ImageType::Zbi);
/// assert_eq!(image, None);
/// ```
pub(crate) fn matches_base(filename: impl Into<String>, imagetype: ImageType) -> Option<Self> {
let filename = filename.into();
match filename.strip_prefix(imagetype.name()) {
None | Some("_") => None,
Some("") => Some(Self { imagetype, filename }),
Some(subtype) if !subtype.starts_with('_') => None,
Some(_) => Some(Self { imagetype, filename }),
}
}
/// The particular type of this image as understood by the paver service, if present.
pub fn subtype(&self) -> Option<&str> {
if self.filename.len() == self.imagetype.name().len() {
return None;
}
Some(&self.filename[(self.imagetype.name().len() + 1)..])
}
/// The filename of the image relative to the update package.
pub fn name(&self) -> &str {
&self.filename
}
}
/// A classification for the type of an image.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageClass {
/// Kernel image
Zbi,
/// Metadata for [`ImageClass::Zbi`]
ZbiVbmeta,
/// Recovery image
Recovery,
/// Metadata for [`ImageClass::Recovery`]
RecoveryVbmeta,
/// Bootloader firmware
Firmware,
}
impl ImageClass {
/// Determines if this image class would target a recovery partition.
pub fn targets_recovery(self) -> bool {
match self {
ImageClass::Recovery | ImageClass::RecoveryVbmeta => true,
ImageClass::Zbi | ImageClass::ZbiVbmeta | ImageClass::Firmware => false,
}
}
}
pub(crate) async fn open(proxy: &DirectoryProxy, image: &Image) -> Result<Buffer, OpenImageError> {
let file =
io_util::directory::open_file(proxy, &image.name(), fidl_fuchsia_io::OPEN_RIGHT_READABLE)
.await
.map_err(OpenImageError::OpenFile)?;
let (status, buffer) = file
.get_buffer(fidl_fuchsia_io::VMO_FLAG_READ)
.await
.map_err(OpenImageError::FidlGetBuffer)?;
Status::ok(status).map_err(OpenImageError::GetBuffer)?;
let buffer = buffer.ok_or(OpenImageError::MissingBuffer)?;
// The paver service requires VMOs that are resizable, and blobfs does not give out resizable
// VMOs. Fortunately, a copy-on-write child clone of the vmo can be made resizable.
let vmo = buffer
.vmo
.create_child(VmoChildOptions::COPY_ON_WRITE | VmoChildOptions::RESIZABLE, 0, buffer.size)
.map_err(OpenImageError::CloneBuffer)?;
Ok(Buffer { size: buffer.size, vmo })
}
#[cfg(test)]
mod tests {
use {
super::*, crate::TestUpdatePackage, matches::assert_matches, proptest::prelude::*,
proptest_derive::Arbitrary,
};
#[test]
fn image_new() {
assert_eq!(
Image::new(ImageType::Zbi, None),
Image { imagetype: ImageType::Zbi, filename: "zbi".to_string() }
);
}
#[test]
fn recovery_images_target_recovery() {
assert!(
Image::new(ImageType::Zedboot, None).classify().targets_recovery(),
"image zedboot should target recovery",
);
assert!(
Image::new(ImageType::ZedbootSigned, None).classify().targets_recovery(),
"image zedboot.signed should target recovery",
);
assert!(
Image::new(ImageType::Recovery, None).classify().targets_recovery(),
"image recovery should target recovery",
);
assert!(
Image::new(ImageType::RecoveryVbmeta, None).classify().targets_recovery(),
"image recovery.vbmeta should target recovery",
);
}
#[test]
fn non_recovery_images_do_not_target_recovery() {
assert!(
!Image::new(ImageType::Zbi, None).classify().targets_recovery(),
"image zbi should not target recovery",
);
assert!(
!Image::new(ImageType::ZbiSigned, None).classify().targets_recovery(),
"image zbi.signed not should target recovery",
);
assert!(
!Image::new(ImageType::FuchsiaVbmeta, None).classify().targets_recovery(),
"image fuchsia.vbmeta should not target recovery",
);
assert!(
!Image::new(ImageType::Firmware, None).classify().targets_recovery(),
"image firmware should not target recovery",
);
}
#[test]
fn image_matches_base() {
assert_eq!(Image::matches_base("foo_bar", ImageType::Zbi), None);
assert_eq!(
Image::matches_base("firmware", ImageType::Firmware),
Some(Image { imagetype: ImageType::Firmware, filename: "firmware".to_string() })
);
assert_eq!(
Image::matches_base("firmware_bar", ImageType::Firmware),
Some(Image { imagetype: ImageType::Firmware, filename: "firmware_bar".to_string() })
);
}
#[test]
fn image_matches_base_rejects_underscore_with_no_subtype() {
assert_eq!(Image::matches_base("firmware_", ImageType::Firmware), None);
}
#[test]
fn image_matches_base_rejects_no_underscore_before_subtype() {
assert_eq!(Image::matches_base("zbi3", ImageType::Zbi), None);
}
#[test]
fn test_image_typed_accessors() {
let image = Image::new(ImageType::Zbi, None);
assert_eq!(image.name(), "zbi");
assert_eq!(image.imagetype(), ImageType::Zbi);
assert_eq!(image.subtype(), None);
let image = Image::new(ImageType::Zbi, Some("ibz"));
assert_eq!(image.name(), "zbi_ibz");
assert_eq!(image.imagetype(), ImageType::Zbi);
assert_eq!(image.subtype(), Some("ibz"));
}
#[derive(Debug, Arbitrary)]
enum ImageConstructor {
New,
MatchesBase,
}
prop_compose! {
fn arb_image()(
constructor: ImageConstructor,
imagetype: ImageType,
subtype: Option<String>,
) -> Image {
let subtype = subtype.as_ref().map(String::as_str);
let image = Image::new(imagetype, subtype);
match constructor {
ImageConstructor::New => image,
ImageConstructor::MatchesBase => {
Image::matches_base(imagetype.name(), imagetype).unwrap()
}
}
}
}
proptest! {
#[test]
fn image_accessors_do_not_panic(image in arb_image()) {
image.name();
image.subtype();
image.classify();
format!("{:?}", image);
}
#[test]
fn filename_starts_with_imagetype(image in arb_image()) {
prop_assert!(image.name().starts_with(image.imagetype().name()));
}
#[test]
fn filename_ends_with_underscore_subtype_if_present(image in arb_image()) {
if let Some(subtype) = image.subtype() {
let suffix = format!("_{}", subtype);
prop_assert!(image.name().ends_with(&suffix));
}
}
}
#[fuchsia_async::run_singlethreaded(test)]
async fn open_present_image_succeeds() {
assert_matches!(
TestUpdatePackage::new()
.add_file("zbi", "zbi contents")
.await
.open_image(&Image::new(ImageType::Zbi, None))
.await,
Ok(_)
);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn open_missing_image_fails() {
assert_matches!(
TestUpdatePackage::new().open_image(&Image::new(ImageType::Zbi, None)).await,
Err(OpenImageError::OpenFile(io_util::node::OpenError::OpenError(Status::NOT_FOUND)))
);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn open_image_buffer_matches_expected_contents() {
let update_pkg = TestUpdatePackage::new().add_file("zbi", "zbi contents").await;
let buffer = update_pkg.open_image(&Image::new(ImageType::Zbi, None)).await.unwrap();
let expected = &b"zbi contents"[..];
assert_eq!(expected.len() as u64, buffer.size);
let mut actual = vec![0; buffer.size as usize];
buffer.vmo.read(&mut actual[..], 0).unwrap();
assert_eq!(expected, actual);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn open_image_buffer_is_resizable() {
let update_pkg = TestUpdatePackage::new().add_file("zbi", "zbi contents").await;
let buffer = update_pkg.open_image(&Image::new(ImageType::Zbi, None)).await.unwrap();
assert_eq!(buffer.vmo.set_size(buffer.size * 2), Ok(()));
}
}