Skip to main content

update_package/
images.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! The images and firmware that should be downloaded and written during the update.
6
7use crate::update_mode::UpdateMode;
8use camino::Utf8Path;
9use fidl_fuchsia_io as fio;
10use fuchsia_url::ParseError;
11use fuchsia_url::fuchsia_pkg::{AbsoluteComponentUrl, PinnedAbsolutePackageUrl};
12use serde::{Deserialize, Serialize};
13use std::collections::{BTreeMap, HashSet};
14use thiserror::Error;
15use zx_status::Status;
16
17/// An error encountered while resolving images.
18#[derive(Debug, Error)]
19#[allow(missing_docs)]
20pub enum ResolveImagesError {
21    #[error("while listing files in the update package")]
22    ListCandidates(#[source] fuchsia_fs::directory::EnumerateError),
23}
24
25/// An error encountered while verifying an [`ImagePackagesSlots`].
26#[derive(Debug, Error, PartialEq, Eq)]
27#[allow(missing_docs)]
28pub enum VerifyError {
29    #[error("images list did not contain an entry for 'zbi'")]
30    MissingZbi,
31
32    #[error("images list unexpectedly contained an entry for 'zbi'")]
33    UnexpectedZbi,
34}
35
36/// An error encountered while handling [`ImageMetadata`].
37#[derive(Debug, Error)]
38#[allow(missing_docs)]
39pub enum ImageMetadataError {
40    #[error("while reading the image")]
41    Io(#[source] std::io::Error),
42
43    #[error("invalid resource path")]
44    InvalidResourcePath(#[source] ParseError),
45}
46
47/// An error encountered while loading the images.json manifest.
48#[derive(Debug, Error)]
49#[allow(missing_docs)]
50pub enum ImagePackagesError {
51    #[error("`images.json` not present in update package")]
52    NotFound,
53
54    #[error("while opening `images.json`")]
55    Open(#[source] fuchsia_fs::node::OpenError),
56
57    #[error("while reading `images.json`")]
58    Read(#[source] fuchsia_fs::file::ReadError),
59
60    #[error("while parsing `images.json`")]
61    Parse(#[source] serde_json::error::Error),
62}
63
64/// A builder of [`ImagePackagesManifest`].
65#[derive(Debug, Clone)]
66pub struct ImagePackagesManifestBuilder {
67    slots: ImagesMetadata,
68}
69
70/// A versioned [`ImagePackagesManifest`].
71#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
72#[serde(tag = "version", content = "contents", deny_unknown_fields)]
73#[allow(missing_docs)]
74pub enum VersionedImagePackagesManifest {
75    #[serde(rename = "1")]
76    Version1(ImagePackagesManifest),
77}
78
79/// A manifest describing the various images and firmware packages that should be fetched and
80/// written during a system update, as well as metadata about those images and where to find them.
81#[derive(Serialize, Debug, PartialEq, Eq, Clone)]
82#[allow(missing_docs)]
83pub struct ImagePackagesManifest {
84    #[serde(rename = "partitions")]
85    pub assets: Vec<AssetMetadata>,
86    pub firmware: Vec<FirmwareMetadata>,
87}
88
89/// Metadata describing a firmware image.
90#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
91#[serde(deny_unknown_fields)]
92#[allow(missing_docs)]
93pub struct FirmwareMetadata {
94    #[serde(rename = "type")]
95    pub type_: String,
96    pub size: u64,
97    #[serde(rename = "hash")]
98    pub sha256: fuchsia_hash::Sha256,
99    pub url: AbsoluteComponentUrl,
100}
101
102/// Metadata describing a Zbi or Vbmeta image, whether or not it is for recovery, and where to
103/// resolve it from.
104#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
105#[serde(deny_unknown_fields)]
106#[allow(missing_docs)]
107pub struct AssetMetadata {
108    pub slot: Slot,
109    #[serde(rename = "type")]
110    pub type_: AssetType,
111    pub size: u64,
112    #[serde(rename = "hash")]
113    pub sha256: fuchsia_hash::Sha256,
114    pub url: AbsoluteComponentUrl,
115}
116
117/// Whether an asset should be written to recovery or the non-current partition.
118#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Hash)]
119#[serde(rename_all = "lowercase")]
120#[allow(missing_docs)]
121pub enum Slot {
122    /// Write the asset to the non-current partition (if ABR is supported, otherwise overwrite
123    /// the current partition).
124    Fuchsia,
125
126    /// Write the asset to the recovery partition.
127    Recovery,
128}
129
130/// Image asset type.
131#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Hash)]
132#[serde(rename_all = "lowercase")]
133#[allow(missing_docs)]
134pub enum AssetType {
135    /// A Zircon Boot Image.
136    Zbi,
137
138    /// Verified Boot Metadata.
139    Vbmeta,
140}
141
142impl From<ImagePackagesManifest> for ImagesMetadata {
143    fn from(manifest: ImagePackagesManifest) -> Self {
144        ImagesMetadata {
145            fuchsia: manifest.fuchsia(),
146            recovery: manifest.recovery(),
147            firmware: manifest.firmware(),
148        }
149    }
150}
151
152/// The metadata for all the images of the OTA, arranged by how the system-updater would write them
153/// to the paver.
154#[derive(Debug, PartialEq, Eq, Clone)]
155pub struct ImagesMetadata {
156    fuchsia: Option<ZbiAndOptionalVbmetaMetadata>,
157    recovery: Option<ZbiAndOptionalVbmetaMetadata>,
158    firmware: BTreeMap<String, ImageMetadata>,
159}
160
161/// Metadata for artifacts unique to an A/B/R boot slot.
162#[derive(Debug, PartialEq, Eq, Clone)]
163pub struct ZbiAndOptionalVbmetaMetadata {
164    /// The zircon boot image.
165    zbi: ImageMetadata,
166
167    /// The optional slot metadata.
168    vbmeta: Option<ImageMetadata>,
169}
170
171impl ZbiAndOptionalVbmetaMetadata {
172    /// Returns an immutable borrow to the ZBI designated in this boot slot.
173    pub fn zbi(&self) -> &ImageMetadata {
174        &self.zbi
175    }
176
177    /// Returns an immutable borrow to the VBMeta designated in this boot slot, if one exists.
178    pub fn vbmeta(&self) -> Option<&ImageMetadata> {
179        self.vbmeta.as_ref()
180    }
181}
182
183/// Metadata necessary to determine if a payload matches an image without needing to have the
184/// actual image.
185#[derive(Debug, PartialEq, Eq, Clone)]
186pub struct ImageMetadata {
187    /// The size of the image, in bytes.
188    size: u64,
189
190    /// The sha256 hash of the image. Note this is not the merkle root. It is the content hash of
191    /// the image.
192    sha256: fuchsia_hash::Sha256,
193
194    /// The URL of the image in its package.
195    url: AbsoluteComponentUrl,
196}
197
198impl FirmwareMetadata {
199    /// Creates a new [`FirmwareMetadata`] from the given image metadata and firmware type.
200    pub fn new_from_metadata(type_: impl Into<String>, metadata: ImageMetadata) -> Self {
201        Self {
202            type_: type_.into(),
203            size: metadata.size,
204            sha256: metadata.sha256,
205            url: metadata.url,
206        }
207    }
208
209    fn key(&self) -> &str {
210        &self.type_
211    }
212
213    /// Returns the [`ImageMetadata`] for this image.
214    pub fn metadata(&self) -> ImageMetadata {
215        ImageMetadata { size: self.size, sha256: self.sha256, url: self.url.clone() }
216    }
217}
218
219impl AssetMetadata {
220    /// Creates a new [`AssetMetadata`] from the given image metadata and target slot/type.
221    pub fn new_from_metadata(slot: Slot, type_: AssetType, metadata: ImageMetadata) -> Self {
222        Self { slot, type_, size: metadata.size, sha256: metadata.sha256, url: metadata.url }
223    }
224
225    fn key(&self) -> (Slot, AssetType) {
226        (self.slot, self.type_)
227    }
228
229    /// Returns the [`ImageMetadata`] for this image.
230    pub fn metadata(&self) -> ImageMetadata {
231        ImageMetadata { size: self.size, sha256: self.sha256, url: self.url.clone() }
232    }
233}
234
235impl ImagePackagesManifest {
236    /// Returns a [`ImagePackagesManifestBuilder`] with no configured images.
237    pub fn builder() -> ImagePackagesManifestBuilder {
238        ImagePackagesManifestBuilder {
239            slots: ImagesMetadata { fuchsia: None, recovery: None, firmware: Default::default() },
240        }
241    }
242
243    fn image(&self, slot: Slot, type_: AssetType) -> Option<&AssetMetadata> {
244        self.assets.iter().find(|image| image.slot == slot && image.type_ == type_)
245    }
246
247    fn image_metadata(&self, slot: Slot, type_: AssetType) -> Option<ImageMetadata> {
248        self.image(slot, type_).map(|image| image.metadata())
249    }
250
251    fn slot_metadata(&self, slot: Slot) -> Option<ZbiAndOptionalVbmetaMetadata> {
252        let zbi = self.image_metadata(slot, AssetType::Zbi);
253        let vbmeta = self.image_metadata(slot, AssetType::Vbmeta);
254
255        zbi.map(|zbi| ZbiAndOptionalVbmetaMetadata { zbi, vbmeta })
256    }
257
258    /// Returns metadata for the fuchsia boot slot, if present.
259    pub fn fuchsia(&self) -> Option<ZbiAndOptionalVbmetaMetadata> {
260        self.slot_metadata(Slot::Fuchsia)
261    }
262
263    /// Returns metadata for the recovery boot slot, if present.
264    pub fn recovery(&self) -> Option<ZbiAndOptionalVbmetaMetadata> {
265        self.slot_metadata(Slot::Recovery)
266    }
267
268    /// Returns metadata for the firmware images.
269    pub fn firmware(&self) -> BTreeMap<String, ImageMetadata> {
270        self.firmware.iter().map(|image| (image.type_.to_owned(), image.metadata())).collect()
271    }
272}
273
274impl ImageMetadata {
275    /// Returns new image metadata that designates the given `size` and `hash`, which can be found
276    /// at the given `url`.
277    pub fn new(size: u64, sha256: fuchsia_hash::Sha256, url: AbsoluteComponentUrl) -> Self {
278        Self { size, sha256, url }
279    }
280
281    /// Returns the size of the image, in bytes.
282    pub fn size(&self) -> u64 {
283        self.size
284    }
285
286    /// Returns the sha256 hash of the image.
287    pub fn sha256(&self) -> fuchsia_hash::Sha256 {
288        self.sha256
289    }
290
291    /// Returns the url of the image.
292    pub fn url(&self) -> &AbsoluteComponentUrl {
293        &self.url
294    }
295
296    /// Compute the size and hash for the image file located at `path`, determining the image's
297    /// fuchsia-pkg URL using the given base `url` and `resource` path within the package.
298    pub fn for_path(
299        path: &Utf8Path,
300        url: PinnedAbsolutePackageUrl,
301        resource: String,
302    ) -> Result<Self, ImageMetadataError> {
303        use sha2::Digest as _;
304
305        // TODO(https://fxbug.dev/535596550): Find a better API making a custom Write impl.
306        struct WriteHasher<'a, D>(&'a mut D);
307        impl<D: sha2::Digest> std::io::Write for WriteHasher<'_, D> {
308            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
309                self.0.update(buf);
310                Ok(buf.len())
311            }
312            fn flush(&mut self) -> std::io::Result<()> {
313                Ok(())
314            }
315        }
316
317        let mut hasher = sha2::Sha256::new();
318        let file = std::fs::File::open(path).map_err(ImageMetadataError::Io)?;
319        let mut reader = std::io::BufReader::new(file);
320        let size = std::io::copy(&mut reader, &mut WriteHasher(&mut hasher))
321            .map_err(ImageMetadataError::Io)?;
322        let sha256 = fuchsia_hash::Sha256::from(*AsRef::<[u8; 32]>::as_ref(&hasher.finalize()));
323
324        let url = AbsoluteComponentUrl::from_package_url_and_resource(url.into(), resource)
325            .map_err(ImageMetadataError::InvalidResourcePath)?;
326
327        Ok(Self { size, sha256, url })
328    }
329}
330
331impl ImagePackagesManifestBuilder {
332    /// Configures the "fuchsia" images package to use the given zbi metadata, and optional
333    /// vbmeta metadata.
334    pub fn fuchsia_package(
335        &mut self,
336        zbi: ImageMetadata,
337        vbmeta: Option<ImageMetadata>,
338    ) -> &mut Self {
339        self.slots.fuchsia = Some(ZbiAndOptionalVbmetaMetadata { zbi, vbmeta });
340        self
341    }
342
343    /// Configures the "recovery" images package to use the given zbi metadata, and optional
344    /// vbmeta metadata.
345    pub fn recovery_package(
346        &mut self,
347        zbi: ImageMetadata,
348        vbmeta: Option<ImageMetadata>,
349    ) -> &mut Self {
350        self.slots.recovery = Some(ZbiAndOptionalVbmetaMetadata { zbi, vbmeta });
351        self
352    }
353
354    /// Configures the "firmware" images package from a BTreeMap of ImageMetadata
355    pub fn firmware_package(&mut self, firmware: BTreeMap<String, ImageMetadata>) -> &mut Self {
356        self.slots.firmware = firmware;
357        self
358    }
359
360    /// Returns the constructed manifest.
361    pub fn build(self) -> VersionedImagePackagesManifest {
362        let mut assets = vec![];
363        let mut firmware = vec![];
364
365        if let Some(slot) = self.slots.fuchsia {
366            assets.push(AssetMetadata::new_from_metadata(Slot::Fuchsia, AssetType::Zbi, slot.zbi));
367            if let Some(vbmeta) = slot.vbmeta {
368                assets.push(AssetMetadata::new_from_metadata(
369                    Slot::Fuchsia,
370                    AssetType::Vbmeta,
371                    vbmeta,
372                ));
373            }
374        }
375
376        if let Some(slot) = self.slots.recovery {
377            assets.push(AssetMetadata::new_from_metadata(Slot::Recovery, AssetType::Zbi, slot.zbi));
378            if let Some(vbmeta) = slot.vbmeta {
379                assets.push(AssetMetadata::new_from_metadata(
380                    Slot::Recovery,
381                    AssetType::Vbmeta,
382                    vbmeta,
383                ));
384            }
385        }
386
387        for (type_, metadata) in self.slots.firmware {
388            firmware.push(FirmwareMetadata::new_from_metadata(type_, metadata));
389        }
390
391        VersionedImagePackagesManifest::Version1(ImagePackagesManifest { assets, firmware })
392    }
393}
394
395impl ImagesMetadata {
396    /// Verify that this image package manifest is appropriate for the given update mode.
397    ///
398    /// * `UpdateMode::Normal` - a non-recovery kernel image is required.
399    /// * `UpdateMode::ForceRecovery` - a non-recovery kernel image must not be present.
400    pub fn verify(&self, mode: UpdateMode) -> Result<(), VerifyError> {
401        let contains_zbi_entry = self.fuchsia.is_some();
402        match mode {
403            UpdateMode::Normal if !contains_zbi_entry => Err(VerifyError::MissingZbi),
404            UpdateMode::ForceRecovery if contains_zbi_entry => Err(VerifyError::UnexpectedZbi),
405            _ => Ok(()),
406        }
407    }
408
409    /// Returns an immutable borrow to the boot slot image package designated as "fuchsia" in this
410    /// image packages manifest.
411    pub fn fuchsia(&self) -> Option<&ZbiAndOptionalVbmetaMetadata> {
412        self.fuchsia.as_ref()
413    }
414
415    /// Returns an immutable borrow to the boot slot image package designated as "recovery" in this
416    /// image packages manifest.
417    pub fn recovery(&self) -> Option<&ZbiAndOptionalVbmetaMetadata> {
418        self.recovery.as_ref()
419    }
420
421    /// Returns an immutable borrow to the boot slot image package designated as "firmware" in this
422    /// image packages manifest.
423    pub fn firmware(&self) -> &BTreeMap<String, ImageMetadata> {
424        &self.firmware
425    }
426}
427
428impl<'de> Deserialize<'de> for ImagePackagesManifest {
429    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
430    where
431        D: serde::Deserializer<'de>,
432    {
433        use serde::de::Error;
434
435        #[derive(Debug, Deserialize)]
436        pub struct DeImagePackagesManifest {
437            partitions: Vec<AssetMetadata>,
438            firmware: Vec<FirmwareMetadata>,
439        }
440
441        let parsed = DeImagePackagesManifest::deserialize(deserializer)?;
442
443        // Check for duplicate image destinations, verify URL always contains a hash,
444        // and check that a zbi is present if vbmeta is present.
445        {
446            let mut keys = HashSet::new();
447            for image in &parsed.partitions {
448                if image.metadata().url().package_url().hash().is_none() {
449                    return Err(D::Error::custom(format!(
450                        "image url {:?} does not contain hash",
451                        image.metadata().url()
452                    )));
453                }
454
455                if !keys.insert(image.key()) {
456                    return Err(D::Error::custom(format!(
457                        "duplicate image entry: {:?}",
458                        image.key()
459                    )));
460                }
461            }
462
463            for slot in [Slot::Fuchsia, Slot::Recovery] {
464                if keys.contains(&(slot, AssetType::Vbmeta))
465                    && !keys.contains(&(slot, AssetType::Zbi))
466                {
467                    return Err(D::Error::custom(format!(
468                        "vbmeta without zbi entry in partition {slot:?}"
469                    )));
470                }
471            }
472        }
473
474        // Check for duplicate firmware destinations and verify that url field contains a  hash.
475        {
476            let mut keys = HashSet::new();
477            for image in &parsed.firmware {
478                if image.metadata().url().package_url().hash().is_none() {
479                    return Err(D::Error::custom(format!(
480                        "firmware url {:?} does not contain hash",
481                        image.metadata().url()
482                    )));
483                }
484
485                if !keys.insert(image.key()) {
486                    return Err(D::Error::custom(format!(
487                        "duplicate firmware entry: {:?}",
488                        image.key()
489                    )));
490                }
491            }
492        }
493
494        Ok(ImagePackagesManifest { assets: parsed.partitions, firmware: parsed.firmware })
495    }
496}
497
498/// Returns structured `images.json` data based on raw file contents.
499pub fn parse_image_packages_json(
500    contents: &[u8],
501) -> Result<ImagePackagesManifest, ImagePackagesError> {
502    let VersionedImagePackagesManifest::Version1(manifest) =
503        serde_json::from_slice(contents).map_err(ImagePackagesError::Parse)?;
504
505    Ok(manifest)
506}
507
508pub(crate) async fn images_metadata(
509    proxy: &fio::DirectoryProxy,
510) -> Result<ImagesMetadata, ImagePackagesError> {
511    image_packages(proxy).await.map(Into::into)
512}
513
514async fn image_packages(
515    proxy: &fio::DirectoryProxy,
516) -> Result<ImagePackagesManifest, ImagePackagesError> {
517    let file = fuchsia_fs::directory::open_file(proxy, "images.json", fio::PERM_READABLE)
518        .await
519        .map_err(|e| match e {
520            fuchsia_fs::node::OpenError::OpenError(Status::NOT_FOUND) => {
521                ImagePackagesError::NotFound
522            }
523            e => ImagePackagesError::Open(e),
524        })?;
525
526    let contents = fuchsia_fs::file::read(&file).await.map_err(ImagePackagesError::Read)?;
527
528    parse_image_packages_json(&contents)
529}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534    use assert_matches::assert_matches;
535    use serde_json::json;
536    use std::fs::File;
537    use std::io::Write;
538    use vfs::file::vmo::read_only;
539    use vfs::pseudo_directory;
540
541    fn sha256(n: u8) -> fuchsia_hash::Sha256 {
542        [n; 32].into()
543    }
544
545    fn sha256str(n: u8) -> String {
546        sha256(n).to_string()
547    }
548
549    fn hashstr(n: u8) -> String {
550        fuchsia_hash::Hash::from([n; 32]).to_string()
551    }
552
553    fn test_url(data: &str) -> AbsoluteComponentUrl {
554        format!("fuchsia-pkg://fuchsia.com/update-images-firmware/0?hash=000000000000000000000000000000000000000000000000000000000000000a#{data}").parse().unwrap()
555    }
556
557    fn image_package_url(name: &str, hash: u8) -> PinnedAbsolutePackageUrl {
558        format!("fuchsia-pkg://fuchsia.com/{name}/0?hash={}", hashstr(hash)).parse().unwrap()
559    }
560
561    fn image_package_resource_url(name: &str, hash: u8, resource: &str) -> AbsoluteComponentUrl {
562        format!("fuchsia-pkg://fuchsia.com/{name}/0?hash={}#{resource}", hashstr(hash))
563            .parse()
564            .unwrap()
565    }
566
567    #[test]
568    fn image_metadata_for_path_empty() {
569        let tmp = tempfile::tempdir().expect("/tmp to exist");
570        let path = Utf8Path::from_path(tmp.path()).unwrap().join("empty");
571        let mut f = File::create(&path).unwrap();
572        f.write_all(b"").unwrap();
573        drop(f);
574
575        let resource = "resource";
576        let url = image_package_url("package", 1);
577
578        assert_eq!(
579            ImageMetadata::for_path(&path, url, resource.to_string()).unwrap(),
580            ImageMetadata::new(
581                0,
582                "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".parse().unwrap(),
583                image_package_resource_url("package", 1, resource)
584            ),
585        );
586    }
587
588    #[test]
589    fn image_metadata_for_path_with_unaligned_data() {
590        let tmp = tempfile::tempdir().expect("/tmp to exist");
591        let path = Utf8Path::from_path(tmp.path()).unwrap().join("empty");
592        let mut f = File::create(&path).unwrap();
593        f.write_all(&[0; 8192 + 4096]).unwrap();
594        drop(f);
595
596        let resource = "resource";
597
598        let url = image_package_url("package", 1);
599
600        assert_eq!(
601            ImageMetadata::for_path(&path, url, resource.to_string()).unwrap(),
602            ImageMetadata::new(
603                8192 + 4096,
604                "f3cc103136423a57975750907ebc1d367e2985ac6338976d4d5a439f50323f4a".parse().unwrap(),
605                image_package_resource_url("package", 1, resource)
606            ),
607        );
608    }
609
610    #[test]
611    fn parses_minimal_manifest() {
612        let raw_json = json!({
613            "version": "1",
614            "contents": { "partitions" : [], "firmware" : []},
615        })
616        .to_string();
617
618        let actual = parse_image_packages_json(raw_json.as_bytes()).unwrap();
619        assert_eq!(actual, ImagePackagesManifest { assets: vec![], firmware: vec![] });
620    }
621
622    #[test]
623    fn builder_builds_minimal() {
624        assert_eq!(
625            ImagePackagesManifest::builder().build(),
626            VersionedImagePackagesManifest::Version1(ImagePackagesManifest {
627                assets: vec![],
628                firmware: vec![],
629            }),
630        );
631    }
632
633    #[test]
634    fn builder_builds_populated_manifest() {
635        let actual = ImagePackagesManifest::builder()
636            .fuchsia_package(
637                ImageMetadata::new(
638                    1,
639                    sha256(1),
640                    image_package_resource_url("update-images-fuchsia", 9, "zbi"),
641                ),
642                Some(ImageMetadata::new(
643                    2,
644                    sha256(2),
645                    image_package_resource_url("update-images-fuchsia", 8, "vbmeta"),
646                )),
647            )
648            .recovery_package(
649                ImageMetadata::new(
650                    3,
651                    sha256(3),
652                    image_package_resource_url("update-images-recovery", 7, "zbi"),
653                ),
654                None,
655            )
656            .firmware_package(BTreeMap::from([
657                (
658                    "".into(),
659                    ImageMetadata::new(
660                        5,
661                        sha256(5),
662                        image_package_resource_url("update-images-firmware", 6, "a"),
663                    ),
664                ),
665                (
666                    "bl2".into(),
667                    ImageMetadata::new(
668                        6,
669                        sha256(6),
670                        image_package_resource_url("update-images-firmware", 5, "b"),
671                    ),
672                ),
673            ]))
674            .clone()
675            .build();
676        assert_eq!(
677            actual,
678            VersionedImagePackagesManifest::Version1(ImagePackagesManifest {
679                assets: vec![
680                    AssetMetadata {
681                        slot: Slot::Fuchsia,
682                        type_: AssetType::Zbi,
683                        size: 1,
684                        sha256: sha256(1),
685                        url: image_package_resource_url("update-images-fuchsia", 9, "zbi"),
686                    },
687                    AssetMetadata {
688                        slot: Slot::Fuchsia,
689                        type_: AssetType::Vbmeta,
690                        size: 2,
691                        sha256: sha256(2),
692                        url: image_package_resource_url("update-images-fuchsia", 8, "vbmeta"),
693                    },
694                    AssetMetadata {
695                        slot: Slot::Recovery,
696                        type_: AssetType::Zbi,
697                        size: 3,
698                        sha256: sha256(3),
699                        url: image_package_resource_url("update-images-recovery", 7, "zbi"),
700                    },
701                ],
702                firmware: vec![
703                    FirmwareMetadata {
704                        type_: "".to_owned(),
705                        size: 5,
706                        sha256: sha256(5),
707                        url: image_package_resource_url("update-images-firmware", 6, "a"),
708                    },
709                    FirmwareMetadata {
710                        type_: "bl2".to_owned(),
711                        size: 6,
712                        sha256: sha256(6),
713                        url: image_package_resource_url("update-images-firmware", 5, "b"),
714                    },
715                ],
716            })
717        );
718    }
719
720    #[test]
721    fn parses_example_manifest() {
722        let raw_json = json!({
723            "version": "1",
724            "contents":  {
725                "partitions": [
726                    {
727                    "slot" : "fuchsia",
728                    "type" : "zbi",
729                    "size" : 1,
730                    "hash" : sha256str(1),
731                    "url" : image_package_resource_url("package", 1, "zbi")
732                }, {
733                    "slot" : "fuchsia",
734                    "type" : "vbmeta",
735                    "size" : 2,
736                    "hash" : sha256str(2),
737                    "url" : image_package_resource_url("package", 1, "vbmeta")
738                },
739                {
740                    "slot" : "recovery",
741                    "type" : "zbi",
742                    "size" : 3,
743                    "hash" : sha256str(3),
744                    "url" : image_package_resource_url("package", 1, "rzbi")
745                }, {
746                    "slot" : "recovery",
747                    "type" : "vbmeta",
748                    "size" : 3,
749                    "hash" : sha256str(3),
750                    "url" : image_package_resource_url("package", 1, "rvbmeta")
751                    },
752                ],
753                "firmware": [
754                    {
755                        "type" : "",
756                        "size" : 5,
757                        "hash" : sha256str(5),
758                        "url" : image_package_resource_url("package", 1, "firmware")
759                    }, {
760                        "type" : "bl2",
761                        "size" : 6,
762                        "hash" : sha256str(6),
763                        "url" : image_package_resource_url("package", 1, "firmware")
764                    },
765                ],
766
767            }
768
769            }
770        )
771        .to_string();
772
773        let actual = parse_image_packages_json(raw_json.as_bytes()).unwrap();
774        assert_eq!(
775            ImagesMetadata::from(actual),
776            ImagesMetadata {
777                fuchsia: Some(ZbiAndOptionalVbmetaMetadata {
778                    zbi: ImageMetadata::new(
779                        1,
780                        sha256(1),
781                        image_package_resource_url("package", 1, "zbi")
782                    ),
783                    vbmeta: Some(ImageMetadata::new(
784                        2,
785                        sha256(2),
786                        image_package_resource_url("package", 1, "vbmeta")
787                    )),
788                },),
789                recovery: Some(ZbiAndOptionalVbmetaMetadata {
790                    zbi: ImageMetadata::new(
791                        3,
792                        sha256(3),
793                        image_package_resource_url("package", 1, "rzbi")
794                    ),
795                    vbmeta: Some(ImageMetadata::new(
796                        3,
797                        sha256(3),
798                        image_package_resource_url("package", 1, "rvbmeta")
799                    )),
800                }),
801                firmware: BTreeMap::from([
802                    (
803                        "".into(),
804                        ImageMetadata::new(
805                            5,
806                            sha256(5),
807                            image_package_resource_url("package", 1, "firmware")
808                        )
809                    ),
810                    (
811                        "bl2".into(),
812                        ImageMetadata::new(
813                            6,
814                            sha256(6),
815                            image_package_resource_url("package", 1, "firmware")
816                        )
817                    ),
818                ])
819            }
820        );
821    }
822
823    #[test]
824    fn rejects_duplicate_image_keys() {
825        let raw_json = json!({
826            "version": "1",
827            "contents":  {
828                "partitions": [ {
829                    "slot" : "fuchsia",
830                    "type" : "zbi",
831                    "size" : 1,
832                    "hash" : sha256str(1),
833                    "url" : image_package_resource_url("package", 1, "zbi")
834                }, {
835                    "slot" : "fuchsia",
836                    "type" : "zbi",
837                    "size" : 1,
838                    "hash" : sha256str(1),
839                    "url" : image_package_resource_url("package", 1, "zbi")
840                },
841                ],
842                "firmware": [],
843            }
844        })
845        .to_string();
846
847        assert_matches!(
848            parse_image_packages_json(raw_json.as_bytes()),
849            Err(ImagePackagesError::Parse(e))
850                if e.to_string().contains("duplicate image entry: (Fuchsia, Zbi)")
851        );
852    }
853
854    #[test]
855    fn rejects_duplicate_firmware_keys() {
856        let raw_json = json!({
857            "version": "1",
858            "contents":  {
859                "partitions": [],
860                "firmware": [
861                    {
862                        "type" : "",
863                        "size" : 5,
864                        "hash" : sha256str(5),
865                        "url" : image_package_resource_url("package", 1, "firmware")
866                    }, {
867                        "type" : "",
868                        "size" : 5,
869                        "hash" : sha256str(5),
870                        "url" : image_package_resource_url("package", 1, "firmware")
871                    },
872                ],
873            }
874        })
875        .to_string();
876
877        assert_matches!(
878            parse_image_packages_json(raw_json.as_bytes()),
879            Err(ImagePackagesError::Parse(e))
880                if e.to_string().contains(r#"duplicate firmware entry: """#)
881        );
882    }
883
884    #[test]
885    fn rejects_vbmeta_without_zbi() {
886        let raw_json = json!({
887            "version": "1",
888            "contents":  {
889                "partitions": [{
890                    "slot" : "fuchsia",
891                    "type" : "vbmeta",
892                    "size" : 1,
893                    "hash" : sha256str(1),
894                    "url" : image_package_resource_url("package", 1, "vbmeta")
895                }],
896                "firmware": [],
897            }
898        })
899        .to_string();
900
901        assert_matches!(
902            parse_image_packages_json(raw_json.as_bytes()),
903            Err(ImagePackagesError::Parse(e))
904                if e.to_string().contains("vbmeta without zbi entry in partition Fuchsia")
905        );
906    }
907
908    #[test]
909    fn rejects_urls_without_hash_partitions() {
910        let raw_json = json!({
911            "version": "1",
912            "contents":  {
913                "partitions": [{
914                    "slot" : "fuchsia",
915                    "type" : "zbi",
916                    "size" : 1,
917                    "hash" : sha256str(1),
918                    "url" : "fuchsia-pkg://fuchsia.com/package/0#zbi"
919                }],
920                "firmware": [],
921            }
922        })
923        .to_string();
924
925        assert_matches!(
926            parse_image_packages_json(raw_json.as_bytes()),
927            Err(ImagePackagesError::Parse(e)) if e.to_string().contains("does not contain hash")
928        );
929    }
930
931    #[test]
932    fn rejects_urls_without_hash_firmware() {
933        let raw_json = json!({
934            "version": "1",
935            "contents":  {
936                "partitions": [],
937                "firmware": [{
938                    "type" : "",
939                    "size" : 5,
940                    "hash" : sha256str(5),
941                    "url" : "fuchsia-pkg://fuchsia.com/package/0#firmware"
942                }],
943            }
944        })
945        .to_string();
946
947        assert_matches!(
948            parse_image_packages_json(raw_json.as_bytes()),
949            Err(ImagePackagesError::Parse(e)) if e.to_string().contains("does not contain hash")
950        );
951    }
952
953    #[test]
954    fn verify_mode_normal_requires_zbi() {
955        let with_zbi = ImagesMetadata {
956            fuchsia: Some(ZbiAndOptionalVbmetaMetadata {
957                zbi: ImageMetadata::new(1, sha256(1), test_url("zbi")),
958                vbmeta: None,
959            }),
960            recovery: None,
961            firmware: BTreeMap::new(),
962        };
963
964        assert_eq!(with_zbi.verify(UpdateMode::Normal), Ok(()));
965
966        let without_zbi =
967            ImagesMetadata { fuchsia: None, recovery: None, firmware: BTreeMap::new() };
968
969        assert_eq!(without_zbi.verify(UpdateMode::Normal), Err(VerifyError::MissingZbi));
970    }
971
972    #[test]
973    fn verify_mode_force_recovery_requires_no_zbi() {
974        let with_zbi = ImagesMetadata {
975            fuchsia: Some(ZbiAndOptionalVbmetaMetadata {
976                zbi: ImageMetadata::new(1, sha256(1), test_url("zbi")),
977                vbmeta: None,
978            }),
979            recovery: None,
980            firmware: BTreeMap::new(),
981        };
982
983        assert_eq!(with_zbi.verify(UpdateMode::ForceRecovery), Err(VerifyError::UnexpectedZbi));
984
985        let without_zbi =
986            ImagesMetadata { fuchsia: None, recovery: None, firmware: BTreeMap::new() };
987
988        assert_eq!(without_zbi.verify(UpdateMode::ForceRecovery), Ok(()));
989    }
990
991    #[fuchsia_async::run_singlethreaded(test)]
992    async fn image_packages_detects_missing_manifest() {
993        let proxy = vfs::directory::serve_read_only(
994            pseudo_directory! {},
995            vfs::execution_scope::ExecutionScope::new(),
996        );
997
998        assert_matches!(image_packages(&proxy).await, Err(ImagePackagesError::NotFound));
999    }
1000
1001    #[fuchsia_async::run_singlethreaded(test)]
1002    async fn image_packages_detects_invalid_json() {
1003        let proxy = vfs::directory::serve_read_only(
1004            pseudo_directory! {
1005                "images.json" => read_only("not json!"),
1006            },
1007            vfs::execution_scope::ExecutionScope::new(),
1008        );
1009
1010        assert_matches!(image_packages(&proxy).await, Err(ImagePackagesError::Parse(_)));
1011    }
1012
1013    #[fuchsia_async::run_singlethreaded(test)]
1014    async fn image_packages_loads_valid_manifest() {
1015        let proxy = vfs::directory::serve_read_only(
1016            pseudo_directory! {
1017                "images.json" => read_only(r#"{
1018"version": "1",
1019"contents": { "partitions" : [], "firmware" : [] }
1020}"#),
1021            },
1022            vfs::execution_scope::ExecutionScope::new(),
1023        );
1024
1025        assert_eq!(
1026            image_packages(&proxy).await.unwrap(),
1027            ImagePackagesManifest { assets: vec![], firmware: vec![] }
1028        );
1029    }
1030
1031    #[fuchsia::test]
1032    fn boot_slot_accessors() {
1033        let slot = ZbiAndOptionalVbmetaMetadata {
1034            zbi: ImageMetadata::new(1, sha256(1), test_url("zbi")),
1035            vbmeta: Some(ImageMetadata::new(2, sha256(2), test_url("vbmeta"))),
1036        };
1037
1038        assert_eq!(slot.zbi(), &ImageMetadata::new(1, sha256(1), test_url("zbi")));
1039        assert_eq!(slot.vbmeta(), Some(&ImageMetadata::new(2, sha256(2), test_url("vbmeta"))));
1040
1041        let slot = ZbiAndOptionalVbmetaMetadata {
1042            zbi: ImageMetadata::new(1, sha256(1), test_url("zbi")),
1043            vbmeta: None,
1044        };
1045        assert_eq!(slot.vbmeta(), None);
1046    }
1047
1048    #[fuchsia::test]
1049    fn image_packages_manifest_accessors() {
1050        let slot = ZbiAndOptionalVbmetaMetadata {
1051            zbi: ImageMetadata::new(1, sha256(1), test_url("zbi")),
1052            vbmeta: Some(ImageMetadata::new(2, sha256(2), test_url("vbmeta"))),
1053        };
1054
1055        let mut builder = ImagePackagesManifest::builder();
1056        builder.fuchsia_package(
1057            ImageMetadata::new(1, sha256(1), test_url("zbi")),
1058            Some(ImageMetadata::new(2, sha256(2), test_url("vbmeta"))),
1059        );
1060        let VersionedImagePackagesManifest::Version1(manifest) = builder.build();
1061
1062        assert_eq!(manifest.fuchsia(), Some(slot.clone()));
1063        assert_eq!(manifest.recovery(), None);
1064        assert_eq!(manifest.firmware(), BTreeMap::new());
1065
1066        let mut builder = ImagePackagesManifest::builder();
1067        builder.recovery_package(
1068            ImageMetadata::new(1, sha256(1), test_url("zbi")),
1069            Some(ImageMetadata::new(2, sha256(2), test_url("vbmeta"))),
1070        );
1071        let VersionedImagePackagesManifest::Version1(manifest) = builder.build();
1072
1073        assert_eq!(manifest.fuchsia(), None);
1074        assert_eq!(manifest.recovery(), Some(slot));
1075        assert_eq!(manifest.firmware(), BTreeMap::new());
1076
1077        let mut builder = ImagePackagesManifest::builder();
1078        builder.firmware_package(BTreeMap::from([(
1079            "".into(),
1080            ImageMetadata::new(
1081                5,
1082                sha256(5),
1083                image_package_resource_url("update-images-firmware", 6, "a"),
1084            ),
1085        )]));
1086        let VersionedImagePackagesManifest::Version1(manifest) = builder.build();
1087        assert_eq!(manifest.fuchsia(), None);
1088        assert_eq!(manifest.recovery(), None);
1089        assert_eq!(
1090            manifest.firmware(),
1091            BTreeMap::from([(
1092                "".into(),
1093                ImageMetadata::new(
1094                    5,
1095                    sha256(5),
1096                    image_package_resource_url("update-images-firmware", 6, "a")
1097                )
1098            )])
1099        )
1100    }
1101
1102    #[fuchsia::test]
1103    fn firmware_image_format_to_image_metadata() {
1104        let assembly_firmware = FirmwareMetadata {
1105            type_: "".to_string(),
1106            size: 1,
1107            sha256: sha256(1),
1108            url: image_package_resource_url("package", 1, "firmware"),
1109        };
1110
1111        let image_meta_data = ImageMetadata {
1112            size: 1,
1113            sha256: sha256(1),
1114            url: image_package_resource_url("package", 1, "firmware"),
1115        };
1116
1117        let firmware_into: ImageMetadata = assembly_firmware.metadata();
1118
1119        assert_eq!(firmware_into, image_meta_data);
1120    }
1121
1122    #[fuchsia::test]
1123    fn assembly_image_format_to_image_metadata() {
1124        let assembly_image = AssetMetadata {
1125            slot: Slot::Fuchsia,
1126            type_: AssetType::Zbi,
1127            size: 1,
1128            sha256: sha256(1),
1129            url: image_package_resource_url("package", 1, "image"),
1130        };
1131
1132        let image_meta_data = ImageMetadata {
1133            size: 1,
1134            sha256: sha256(1),
1135            url: image_package_resource_url("package", 1, "image"),
1136        };
1137
1138        let image_into: ImageMetadata = assembly_image.metadata();
1139
1140        assert_eq!(image_into, image_meta_data);
1141    }
1142
1143    #[fuchsia::test]
1144    fn manifest_conversion_minimal() {
1145        let manifest = ImagePackagesManifest { assets: vec![], firmware: vec![] };
1146
1147        let slots = ImagesMetadata { fuchsia: None, recovery: None, firmware: BTreeMap::new() };
1148
1149        let translated_manifest: ImagesMetadata = manifest.into();
1150        assert_eq!(translated_manifest, slots);
1151    }
1152
1153    #[fuchsia::test]
1154    fn manifest_conversion_maximal() {
1155        let manifest = ImagePackagesManifest {
1156            assets: vec![
1157                AssetMetadata {
1158                    slot: Slot::Fuchsia,
1159                    type_: AssetType::Zbi,
1160                    size: 1,
1161                    sha256: sha256(1),
1162                    url: test_url("1"),
1163                },
1164                AssetMetadata {
1165                    slot: Slot::Fuchsia,
1166                    type_: AssetType::Vbmeta,
1167                    size: 2,
1168                    sha256: sha256(2),
1169                    url: test_url("2"),
1170                },
1171                AssetMetadata {
1172                    slot: Slot::Recovery,
1173                    type_: AssetType::Zbi,
1174                    size: 3,
1175                    sha256: sha256(3),
1176                    url: test_url("3"),
1177                },
1178                AssetMetadata {
1179                    slot: Slot::Recovery,
1180                    type_: AssetType::Vbmeta,
1181                    size: 4,
1182                    sha256: sha256(4),
1183                    url: test_url("4"),
1184                },
1185            ],
1186            firmware: vec![
1187                FirmwareMetadata {
1188                    type_: "".to_string(),
1189                    size: 5,
1190                    sha256: sha256(5),
1191                    url: test_url("5"),
1192                },
1193                FirmwareMetadata {
1194                    type_: "bl2".to_string(),
1195                    size: 6,
1196                    sha256: sha256(6),
1197                    url: test_url("6"),
1198                },
1199            ],
1200        };
1201
1202        let slots = ImagesMetadata {
1203            fuchsia: Some(ZbiAndOptionalVbmetaMetadata {
1204                zbi: ImageMetadata::new(1, sha256(1), test_url("1")),
1205                vbmeta: Some(ImageMetadata::new(2, sha256(2), test_url("2"))),
1206            }),
1207            recovery: Some(ZbiAndOptionalVbmetaMetadata {
1208                zbi: ImageMetadata::new(3, sha256(3), test_url("3")),
1209                vbmeta: Some(ImageMetadata::new(4, sha256(4), test_url("4"))),
1210            }),
1211            firmware: BTreeMap::from([
1212                ("".into(), ImageMetadata::new(5, sha256(5), test_url("5"))),
1213                ("bl2".into(), ImageMetadata::new(6, sha256(6), test_url("6"))),
1214            ]),
1215        };
1216
1217        let translated_manifest: ImagesMetadata = manifest.into();
1218        assert_eq!(translated_manifest, slots);
1219    }
1220}