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        let mut hasher = sha2::Sha256::new();
306        let mut file = std::fs::File::open(path).map_err(ImageMetadataError::Io)?;
307        let size = std::io::copy(&mut file, &mut hasher).map_err(ImageMetadataError::Io)?;
308        let sha256 = fuchsia_hash::Sha256::from(*AsRef::<[u8; 32]>::as_ref(&hasher.finalize()));
309
310        let url = AbsoluteComponentUrl::from_package_url_and_resource(url.into(), resource)
311            .map_err(ImageMetadataError::InvalidResourcePath)?;
312
313        Ok(Self { size, sha256, url })
314    }
315}
316
317impl ImagePackagesManifestBuilder {
318    /// Configures the "fuchsia" images package to use the given zbi metadata, and optional
319    /// vbmeta metadata.
320    pub fn fuchsia_package(
321        &mut self,
322        zbi: ImageMetadata,
323        vbmeta: Option<ImageMetadata>,
324    ) -> &mut Self {
325        self.slots.fuchsia = Some(ZbiAndOptionalVbmetaMetadata { zbi, vbmeta });
326        self
327    }
328
329    /// Configures the "recovery" images package to use the given zbi metadata, and optional
330    /// vbmeta metadata.
331    pub fn recovery_package(
332        &mut self,
333        zbi: ImageMetadata,
334        vbmeta: Option<ImageMetadata>,
335    ) -> &mut Self {
336        self.slots.recovery = Some(ZbiAndOptionalVbmetaMetadata { zbi, vbmeta });
337        self
338    }
339
340    /// Configures the "firmware" images package from a BTreeMap of ImageMetadata
341    pub fn firmware_package(&mut self, firmware: BTreeMap<String, ImageMetadata>) -> &mut Self {
342        self.slots.firmware = firmware;
343        self
344    }
345
346    /// Returns the constructed manifest.
347    pub fn build(self) -> VersionedImagePackagesManifest {
348        let mut assets = vec![];
349        let mut firmware = vec![];
350
351        if let Some(slot) = self.slots.fuchsia {
352            assets.push(AssetMetadata::new_from_metadata(Slot::Fuchsia, AssetType::Zbi, slot.zbi));
353            if let Some(vbmeta) = slot.vbmeta {
354                assets.push(AssetMetadata::new_from_metadata(
355                    Slot::Fuchsia,
356                    AssetType::Vbmeta,
357                    vbmeta,
358                ));
359            }
360        }
361
362        if let Some(slot) = self.slots.recovery {
363            assets.push(AssetMetadata::new_from_metadata(Slot::Recovery, AssetType::Zbi, slot.zbi));
364            if let Some(vbmeta) = slot.vbmeta {
365                assets.push(AssetMetadata::new_from_metadata(
366                    Slot::Recovery,
367                    AssetType::Vbmeta,
368                    vbmeta,
369                ));
370            }
371        }
372
373        for (type_, metadata) in self.slots.firmware {
374            firmware.push(FirmwareMetadata::new_from_metadata(type_, metadata));
375        }
376
377        VersionedImagePackagesManifest::Version1(ImagePackagesManifest { assets, firmware })
378    }
379}
380
381impl ImagesMetadata {
382    /// Verify that this image package manifest is appropriate for the given update mode.
383    ///
384    /// * `UpdateMode::Normal` - a non-recovery kernel image is required.
385    /// * `UpdateMode::ForceRecovery` - a non-recovery kernel image must not be present.
386    pub fn verify(&self, mode: UpdateMode) -> Result<(), VerifyError> {
387        let contains_zbi_entry = self.fuchsia.is_some();
388        match mode {
389            UpdateMode::Normal if !contains_zbi_entry => Err(VerifyError::MissingZbi),
390            UpdateMode::ForceRecovery if contains_zbi_entry => Err(VerifyError::UnexpectedZbi),
391            _ => Ok(()),
392        }
393    }
394
395    /// Returns an immutable borrow to the boot slot image package designated as "fuchsia" in this
396    /// image packages manifest.
397    pub fn fuchsia(&self) -> Option<&ZbiAndOptionalVbmetaMetadata> {
398        self.fuchsia.as_ref()
399    }
400
401    /// Returns an immutable borrow to the boot slot image package designated as "recovery" in this
402    /// image packages manifest.
403    pub fn recovery(&self) -> Option<&ZbiAndOptionalVbmetaMetadata> {
404        self.recovery.as_ref()
405    }
406
407    /// Returns an immutable borrow to the boot slot image package designated as "firmware" in this
408    /// image packages manifest.
409    pub fn firmware(&self) -> &BTreeMap<String, ImageMetadata> {
410        &self.firmware
411    }
412}
413
414impl<'de> Deserialize<'de> for ImagePackagesManifest {
415    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
416    where
417        D: serde::Deserializer<'de>,
418    {
419        use serde::de::Error;
420
421        #[derive(Debug, Deserialize)]
422        pub struct DeImagePackagesManifest {
423            partitions: Vec<AssetMetadata>,
424            firmware: Vec<FirmwareMetadata>,
425        }
426
427        let parsed = DeImagePackagesManifest::deserialize(deserializer)?;
428
429        // Check for duplicate image destinations, verify URL always contains a hash,
430        // and check that a zbi is present if vbmeta is present.
431        {
432            let mut keys = HashSet::new();
433            for image in &parsed.partitions {
434                if image.metadata().url().package_url().hash().is_none() {
435                    return Err(D::Error::custom(format!(
436                        "image url {:?} does not contain hash",
437                        image.metadata().url()
438                    )));
439                }
440
441                if !keys.insert(image.key()) {
442                    return Err(D::Error::custom(format!(
443                        "duplicate image entry: {:?}",
444                        image.key()
445                    )));
446                }
447            }
448
449            for slot in [Slot::Fuchsia, Slot::Recovery] {
450                if keys.contains(&(slot, AssetType::Vbmeta))
451                    && !keys.contains(&(slot, AssetType::Zbi))
452                {
453                    return Err(D::Error::custom(format!(
454                        "vbmeta without zbi entry in partition {slot:?}"
455                    )));
456                }
457            }
458        }
459
460        // Check for duplicate firmware destinations and verify that url field contains a  hash.
461        {
462            let mut keys = HashSet::new();
463            for image in &parsed.firmware {
464                if image.metadata().url().package_url().hash().is_none() {
465                    return Err(D::Error::custom(format!(
466                        "firmware url {:?} does not contain hash",
467                        image.metadata().url()
468                    )));
469                }
470
471                if !keys.insert(image.key()) {
472                    return Err(D::Error::custom(format!(
473                        "duplicate firmware entry: {:?}",
474                        image.key()
475                    )));
476                }
477            }
478        }
479
480        Ok(ImagePackagesManifest { assets: parsed.partitions, firmware: parsed.firmware })
481    }
482}
483
484/// Returns structured `images.json` data based on raw file contents.
485pub fn parse_image_packages_json(
486    contents: &[u8],
487) -> Result<ImagePackagesManifest, ImagePackagesError> {
488    let VersionedImagePackagesManifest::Version1(manifest) =
489        serde_json::from_slice(contents).map_err(ImagePackagesError::Parse)?;
490
491    Ok(manifest)
492}
493
494pub(crate) async fn images_metadata(
495    proxy: &fio::DirectoryProxy,
496) -> Result<ImagesMetadata, ImagePackagesError> {
497    image_packages(proxy).await.map(Into::into)
498}
499
500async fn image_packages(
501    proxy: &fio::DirectoryProxy,
502) -> Result<ImagePackagesManifest, ImagePackagesError> {
503    let file = fuchsia_fs::directory::open_file(proxy, "images.json", fio::PERM_READABLE)
504        .await
505        .map_err(|e| match e {
506            fuchsia_fs::node::OpenError::OpenError(Status::NOT_FOUND) => {
507                ImagePackagesError::NotFound
508            }
509            e => ImagePackagesError::Open(e),
510        })?;
511
512    let contents = fuchsia_fs::file::read(&file).await.map_err(ImagePackagesError::Read)?;
513
514    parse_image_packages_json(&contents)
515}
516
517#[cfg(test)]
518mod tests {
519    use super::*;
520    use assert_matches::assert_matches;
521    use serde_json::json;
522    use std::fs::File;
523    use std::io::Write;
524    use vfs::file::vmo::read_only;
525    use vfs::pseudo_directory;
526
527    fn sha256(n: u8) -> fuchsia_hash::Sha256 {
528        [n; 32].into()
529    }
530
531    fn sha256str(n: u8) -> String {
532        sha256(n).to_string()
533    }
534
535    fn hashstr(n: u8) -> String {
536        fuchsia_hash::Hash::from([n; 32]).to_string()
537    }
538
539    fn test_url(data: &str) -> AbsoluteComponentUrl {
540        format!("fuchsia-pkg://fuchsia.com/update-images-firmware/0?hash=000000000000000000000000000000000000000000000000000000000000000a#{data}").parse().unwrap()
541    }
542
543    fn image_package_url(name: &str, hash: u8) -> PinnedAbsolutePackageUrl {
544        format!("fuchsia-pkg://fuchsia.com/{name}/0?hash={}", hashstr(hash)).parse().unwrap()
545    }
546
547    fn image_package_resource_url(name: &str, hash: u8, resource: &str) -> AbsoluteComponentUrl {
548        format!("fuchsia-pkg://fuchsia.com/{name}/0?hash={}#{resource}", hashstr(hash))
549            .parse()
550            .unwrap()
551    }
552
553    #[test]
554    fn image_metadata_for_path_empty() {
555        let tmp = tempfile::tempdir().expect("/tmp to exist");
556        let path = Utf8Path::from_path(tmp.path()).unwrap().join("empty");
557        let mut f = File::create(&path).unwrap();
558        f.write_all(b"").unwrap();
559        drop(f);
560
561        let resource = "resource";
562        let url = image_package_url("package", 1);
563
564        assert_eq!(
565            ImageMetadata::for_path(&path, url, resource.to_string()).unwrap(),
566            ImageMetadata::new(
567                0,
568                "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".parse().unwrap(),
569                image_package_resource_url("package", 1, resource)
570            ),
571        );
572    }
573
574    #[test]
575    fn image_metadata_for_path_with_unaligned_data() {
576        let tmp = tempfile::tempdir().expect("/tmp to exist");
577        let path = Utf8Path::from_path(tmp.path()).unwrap().join("empty");
578        let mut f = File::create(&path).unwrap();
579        f.write_all(&[0; 8192 + 4096]).unwrap();
580        drop(f);
581
582        let resource = "resource";
583
584        let url = image_package_url("package", 1);
585
586        assert_eq!(
587            ImageMetadata::for_path(&path, url, resource.to_string()).unwrap(),
588            ImageMetadata::new(
589                8192 + 4096,
590                "f3cc103136423a57975750907ebc1d367e2985ac6338976d4d5a439f50323f4a".parse().unwrap(),
591                image_package_resource_url("package", 1, resource)
592            ),
593        );
594    }
595
596    #[test]
597    fn parses_minimal_manifest() {
598        let raw_json = json!({
599            "version": "1",
600            "contents": { "partitions" : [], "firmware" : []},
601        })
602        .to_string();
603
604        let actual = parse_image_packages_json(raw_json.as_bytes()).unwrap();
605        assert_eq!(actual, ImagePackagesManifest { assets: vec![], firmware: vec![] });
606    }
607
608    #[test]
609    fn builder_builds_minimal() {
610        assert_eq!(
611            ImagePackagesManifest::builder().build(),
612            VersionedImagePackagesManifest::Version1(ImagePackagesManifest {
613                assets: vec![],
614                firmware: vec![],
615            }),
616        );
617    }
618
619    #[test]
620    fn builder_builds_populated_manifest() {
621        let actual = ImagePackagesManifest::builder()
622            .fuchsia_package(
623                ImageMetadata::new(
624                    1,
625                    sha256(1),
626                    image_package_resource_url("update-images-fuchsia", 9, "zbi"),
627                ),
628                Some(ImageMetadata::new(
629                    2,
630                    sha256(2),
631                    image_package_resource_url("update-images-fuchsia", 8, "vbmeta"),
632                )),
633            )
634            .recovery_package(
635                ImageMetadata::new(
636                    3,
637                    sha256(3),
638                    image_package_resource_url("update-images-recovery", 7, "zbi"),
639                ),
640                None,
641            )
642            .firmware_package(BTreeMap::from([
643                (
644                    "".into(),
645                    ImageMetadata::new(
646                        5,
647                        sha256(5),
648                        image_package_resource_url("update-images-firmware", 6, "a"),
649                    ),
650                ),
651                (
652                    "bl2".into(),
653                    ImageMetadata::new(
654                        6,
655                        sha256(6),
656                        image_package_resource_url("update-images-firmware", 5, "b"),
657                    ),
658                ),
659            ]))
660            .clone()
661            .build();
662        assert_eq!(
663            actual,
664            VersionedImagePackagesManifest::Version1(ImagePackagesManifest {
665                assets: vec![
666                    AssetMetadata {
667                        slot: Slot::Fuchsia,
668                        type_: AssetType::Zbi,
669                        size: 1,
670                        sha256: sha256(1),
671                        url: image_package_resource_url("update-images-fuchsia", 9, "zbi"),
672                    },
673                    AssetMetadata {
674                        slot: Slot::Fuchsia,
675                        type_: AssetType::Vbmeta,
676                        size: 2,
677                        sha256: sha256(2),
678                        url: image_package_resource_url("update-images-fuchsia", 8, "vbmeta"),
679                    },
680                    AssetMetadata {
681                        slot: Slot::Recovery,
682                        type_: AssetType::Zbi,
683                        size: 3,
684                        sha256: sha256(3),
685                        url: image_package_resource_url("update-images-recovery", 7, "zbi"),
686                    },
687                ],
688                firmware: vec![
689                    FirmwareMetadata {
690                        type_: "".to_owned(),
691                        size: 5,
692                        sha256: sha256(5),
693                        url: image_package_resource_url("update-images-firmware", 6, "a"),
694                    },
695                    FirmwareMetadata {
696                        type_: "bl2".to_owned(),
697                        size: 6,
698                        sha256: sha256(6),
699                        url: image_package_resource_url("update-images-firmware", 5, "b"),
700                    },
701                ],
702            })
703        );
704    }
705
706    #[test]
707    fn parses_example_manifest() {
708        let raw_json = json!({
709            "version": "1",
710            "contents":  {
711                "partitions": [
712                    {
713                    "slot" : "fuchsia",
714                    "type" : "zbi",
715                    "size" : 1,
716                    "hash" : sha256str(1),
717                    "url" : image_package_resource_url("package", 1, "zbi")
718                }, {
719                    "slot" : "fuchsia",
720                    "type" : "vbmeta",
721                    "size" : 2,
722                    "hash" : sha256str(2),
723                    "url" : image_package_resource_url("package", 1, "vbmeta")
724                },
725                {
726                    "slot" : "recovery",
727                    "type" : "zbi",
728                    "size" : 3,
729                    "hash" : sha256str(3),
730                    "url" : image_package_resource_url("package", 1, "rzbi")
731                }, {
732                    "slot" : "recovery",
733                    "type" : "vbmeta",
734                    "size" : 3,
735                    "hash" : sha256str(3),
736                    "url" : image_package_resource_url("package", 1, "rvbmeta")
737                    },
738                ],
739                "firmware": [
740                    {
741                        "type" : "",
742                        "size" : 5,
743                        "hash" : sha256str(5),
744                        "url" : image_package_resource_url("package", 1, "firmware")
745                    }, {
746                        "type" : "bl2",
747                        "size" : 6,
748                        "hash" : sha256str(6),
749                        "url" : image_package_resource_url("package", 1, "firmware")
750                    },
751                ],
752
753            }
754
755            }
756        )
757        .to_string();
758
759        let actual = parse_image_packages_json(raw_json.as_bytes()).unwrap();
760        assert_eq!(
761            ImagesMetadata::from(actual),
762            ImagesMetadata {
763                fuchsia: Some(ZbiAndOptionalVbmetaMetadata {
764                    zbi: ImageMetadata::new(
765                        1,
766                        sha256(1),
767                        image_package_resource_url("package", 1, "zbi")
768                    ),
769                    vbmeta: Some(ImageMetadata::new(
770                        2,
771                        sha256(2),
772                        image_package_resource_url("package", 1, "vbmeta")
773                    )),
774                },),
775                recovery: Some(ZbiAndOptionalVbmetaMetadata {
776                    zbi: ImageMetadata::new(
777                        3,
778                        sha256(3),
779                        image_package_resource_url("package", 1, "rzbi")
780                    ),
781                    vbmeta: Some(ImageMetadata::new(
782                        3,
783                        sha256(3),
784                        image_package_resource_url("package", 1, "rvbmeta")
785                    )),
786                }),
787                firmware: BTreeMap::from([
788                    (
789                        "".into(),
790                        ImageMetadata::new(
791                            5,
792                            sha256(5),
793                            image_package_resource_url("package", 1, "firmware")
794                        )
795                    ),
796                    (
797                        "bl2".into(),
798                        ImageMetadata::new(
799                            6,
800                            sha256(6),
801                            image_package_resource_url("package", 1, "firmware")
802                        )
803                    ),
804                ])
805            }
806        );
807    }
808
809    #[test]
810    fn rejects_duplicate_image_keys() {
811        let raw_json = json!({
812            "version": "1",
813            "contents":  {
814                "partitions": [ {
815                    "slot" : "fuchsia",
816                    "type" : "zbi",
817                    "size" : 1,
818                    "hash" : sha256str(1),
819                    "url" : image_package_resource_url("package", 1, "zbi")
820                }, {
821                    "slot" : "fuchsia",
822                    "type" : "zbi",
823                    "size" : 1,
824                    "hash" : sha256str(1),
825                    "url" : image_package_resource_url("package", 1, "zbi")
826                },
827                ],
828                "firmware": [],
829            }
830        })
831        .to_string();
832
833        assert_matches!(
834            parse_image_packages_json(raw_json.as_bytes()),
835            Err(ImagePackagesError::Parse(e))
836                if e.to_string().contains("duplicate image entry: (Fuchsia, Zbi)")
837        );
838    }
839
840    #[test]
841    fn rejects_duplicate_firmware_keys() {
842        let raw_json = json!({
843            "version": "1",
844            "contents":  {
845                "partitions": [],
846                "firmware": [
847                    {
848                        "type" : "",
849                        "size" : 5,
850                        "hash" : sha256str(5),
851                        "url" : image_package_resource_url("package", 1, "firmware")
852                    }, {
853                        "type" : "",
854                        "size" : 5,
855                        "hash" : sha256str(5),
856                        "url" : image_package_resource_url("package", 1, "firmware")
857                    },
858                ],
859            }
860        })
861        .to_string();
862
863        assert_matches!(
864            parse_image_packages_json(raw_json.as_bytes()),
865            Err(ImagePackagesError::Parse(e))
866                if e.to_string().contains(r#"duplicate firmware entry: """#)
867        );
868    }
869
870    #[test]
871    fn rejects_vbmeta_without_zbi() {
872        let raw_json = json!({
873            "version": "1",
874            "contents":  {
875                "partitions": [{
876                    "slot" : "fuchsia",
877                    "type" : "vbmeta",
878                    "size" : 1,
879                    "hash" : sha256str(1),
880                    "url" : image_package_resource_url("package", 1, "vbmeta")
881                }],
882                "firmware": [],
883            }
884        })
885        .to_string();
886
887        assert_matches!(
888            parse_image_packages_json(raw_json.as_bytes()),
889            Err(ImagePackagesError::Parse(e))
890                if e.to_string().contains("vbmeta without zbi entry in partition Fuchsia")
891        );
892    }
893
894    #[test]
895    fn rejects_urls_without_hash_partitions() {
896        let raw_json = json!({
897            "version": "1",
898            "contents":  {
899                "partitions": [{
900                    "slot" : "fuchsia",
901                    "type" : "zbi",
902                    "size" : 1,
903                    "hash" : sha256str(1),
904                    "url" : "fuchsia-pkg://fuchsia.com/package/0#zbi"
905                }],
906                "firmware": [],
907            }
908        })
909        .to_string();
910
911        assert_matches!(
912            parse_image_packages_json(raw_json.as_bytes()),
913            Err(ImagePackagesError::Parse(e)) if e.to_string().contains("does not contain hash")
914        );
915    }
916
917    #[test]
918    fn rejects_urls_without_hash_firmware() {
919        let raw_json = json!({
920            "version": "1",
921            "contents":  {
922                "partitions": [],
923                "firmware": [{
924                    "type" : "",
925                    "size" : 5,
926                    "hash" : sha256str(5),
927                    "url" : "fuchsia-pkg://fuchsia.com/package/0#firmware"
928                }],
929            }
930        })
931        .to_string();
932
933        assert_matches!(
934            parse_image_packages_json(raw_json.as_bytes()),
935            Err(ImagePackagesError::Parse(e)) if e.to_string().contains("does not contain hash")
936        );
937    }
938
939    #[test]
940    fn verify_mode_normal_requires_zbi() {
941        let with_zbi = ImagesMetadata {
942            fuchsia: Some(ZbiAndOptionalVbmetaMetadata {
943                zbi: ImageMetadata::new(1, sha256(1), test_url("zbi")),
944                vbmeta: None,
945            }),
946            recovery: None,
947            firmware: BTreeMap::new(),
948        };
949
950        assert_eq!(with_zbi.verify(UpdateMode::Normal), Ok(()));
951
952        let without_zbi =
953            ImagesMetadata { fuchsia: None, recovery: None, firmware: BTreeMap::new() };
954
955        assert_eq!(without_zbi.verify(UpdateMode::Normal), Err(VerifyError::MissingZbi));
956    }
957
958    #[test]
959    fn verify_mode_force_recovery_requires_no_zbi() {
960        let with_zbi = ImagesMetadata {
961            fuchsia: Some(ZbiAndOptionalVbmetaMetadata {
962                zbi: ImageMetadata::new(1, sha256(1), test_url("zbi")),
963                vbmeta: None,
964            }),
965            recovery: None,
966            firmware: BTreeMap::new(),
967        };
968
969        assert_eq!(with_zbi.verify(UpdateMode::ForceRecovery), Err(VerifyError::UnexpectedZbi));
970
971        let without_zbi =
972            ImagesMetadata { fuchsia: None, recovery: None, firmware: BTreeMap::new() };
973
974        assert_eq!(without_zbi.verify(UpdateMode::ForceRecovery), Ok(()));
975    }
976
977    #[fuchsia_async::run_singlethreaded(test)]
978    async fn image_packages_detects_missing_manifest() {
979        let proxy = vfs::directory::serve_read_only(pseudo_directory! {});
980
981        assert_matches!(image_packages(&proxy).await, Err(ImagePackagesError::NotFound));
982    }
983
984    #[fuchsia_async::run_singlethreaded(test)]
985    async fn image_packages_detects_invalid_json() {
986        let proxy = vfs::directory::serve_read_only(pseudo_directory! {
987            "images.json" => read_only("not json!"),
988        });
989
990        assert_matches!(image_packages(&proxy).await, Err(ImagePackagesError::Parse(_)));
991    }
992
993    #[fuchsia_async::run_singlethreaded(test)]
994    async fn image_packages_loads_valid_manifest() {
995        let proxy = vfs::directory::serve_read_only(pseudo_directory! {
996            "images.json" => read_only(r#"{
997"version": "1",
998"contents": { "partitions" : [], "firmware" : [] }
999}"#),
1000        });
1001
1002        assert_eq!(
1003            image_packages(&proxy).await.unwrap(),
1004            ImagePackagesManifest { assets: vec![], firmware: vec![] }
1005        );
1006    }
1007
1008    #[fuchsia::test]
1009    fn boot_slot_accessors() {
1010        let slot = ZbiAndOptionalVbmetaMetadata {
1011            zbi: ImageMetadata::new(1, sha256(1), test_url("zbi")),
1012            vbmeta: Some(ImageMetadata::new(2, sha256(2), test_url("vbmeta"))),
1013        };
1014
1015        assert_eq!(slot.zbi(), &ImageMetadata::new(1, sha256(1), test_url("zbi")));
1016        assert_eq!(slot.vbmeta(), Some(&ImageMetadata::new(2, sha256(2), test_url("vbmeta"))));
1017
1018        let slot = ZbiAndOptionalVbmetaMetadata {
1019            zbi: ImageMetadata::new(1, sha256(1), test_url("zbi")),
1020            vbmeta: None,
1021        };
1022        assert_eq!(slot.vbmeta(), None);
1023    }
1024
1025    #[fuchsia::test]
1026    fn image_packages_manifest_accessors() {
1027        let slot = ZbiAndOptionalVbmetaMetadata {
1028            zbi: ImageMetadata::new(1, sha256(1), test_url("zbi")),
1029            vbmeta: Some(ImageMetadata::new(2, sha256(2), test_url("vbmeta"))),
1030        };
1031
1032        let mut builder = ImagePackagesManifest::builder();
1033        builder.fuchsia_package(
1034            ImageMetadata::new(1, sha256(1), test_url("zbi")),
1035            Some(ImageMetadata::new(2, sha256(2), test_url("vbmeta"))),
1036        );
1037        let VersionedImagePackagesManifest::Version1(manifest) = builder.build();
1038
1039        assert_eq!(manifest.fuchsia(), Some(slot.clone()));
1040        assert_eq!(manifest.recovery(), None);
1041        assert_eq!(manifest.firmware(), BTreeMap::new());
1042
1043        let mut builder = ImagePackagesManifest::builder();
1044        builder.recovery_package(
1045            ImageMetadata::new(1, sha256(1), test_url("zbi")),
1046            Some(ImageMetadata::new(2, sha256(2), test_url("vbmeta"))),
1047        );
1048        let VersionedImagePackagesManifest::Version1(manifest) = builder.build();
1049
1050        assert_eq!(manifest.fuchsia(), None);
1051        assert_eq!(manifest.recovery(), Some(slot));
1052        assert_eq!(manifest.firmware(), BTreeMap::new());
1053
1054        let mut builder = ImagePackagesManifest::builder();
1055        builder.firmware_package(BTreeMap::from([(
1056            "".into(),
1057            ImageMetadata::new(
1058                5,
1059                sha256(5),
1060                image_package_resource_url("update-images-firmware", 6, "a"),
1061            ),
1062        )]));
1063        let VersionedImagePackagesManifest::Version1(manifest) = builder.build();
1064        assert_eq!(manifest.fuchsia(), None);
1065        assert_eq!(manifest.recovery(), None);
1066        assert_eq!(
1067            manifest.firmware(),
1068            BTreeMap::from([(
1069                "".into(),
1070                ImageMetadata::new(
1071                    5,
1072                    sha256(5),
1073                    image_package_resource_url("update-images-firmware", 6, "a")
1074                )
1075            )])
1076        )
1077    }
1078
1079    #[fuchsia::test]
1080    fn firmware_image_format_to_image_metadata() {
1081        let assembly_firmware = FirmwareMetadata {
1082            type_: "".to_string(),
1083            size: 1,
1084            sha256: sha256(1),
1085            url: image_package_resource_url("package", 1, "firmware"),
1086        };
1087
1088        let image_meta_data = ImageMetadata {
1089            size: 1,
1090            sha256: sha256(1),
1091            url: image_package_resource_url("package", 1, "firmware"),
1092        };
1093
1094        let firmware_into: ImageMetadata = assembly_firmware.metadata();
1095
1096        assert_eq!(firmware_into, image_meta_data);
1097    }
1098
1099    #[fuchsia::test]
1100    fn assembly_image_format_to_image_metadata() {
1101        let assembly_image = AssetMetadata {
1102            slot: Slot::Fuchsia,
1103            type_: AssetType::Zbi,
1104            size: 1,
1105            sha256: sha256(1),
1106            url: image_package_resource_url("package", 1, "image"),
1107        };
1108
1109        let image_meta_data = ImageMetadata {
1110            size: 1,
1111            sha256: sha256(1),
1112            url: image_package_resource_url("package", 1, "image"),
1113        };
1114
1115        let image_into: ImageMetadata = assembly_image.metadata();
1116
1117        assert_eq!(image_into, image_meta_data);
1118    }
1119
1120    #[fuchsia::test]
1121    fn manifest_conversion_minimal() {
1122        let manifest = ImagePackagesManifest { assets: vec![], firmware: vec![] };
1123
1124        let slots = ImagesMetadata { fuchsia: None, recovery: None, firmware: BTreeMap::new() };
1125
1126        let translated_manifest: ImagesMetadata = manifest.into();
1127        assert_eq!(translated_manifest, slots);
1128    }
1129
1130    #[fuchsia::test]
1131    fn manifest_conversion_maximal() {
1132        let manifest = ImagePackagesManifest {
1133            assets: vec![
1134                AssetMetadata {
1135                    slot: Slot::Fuchsia,
1136                    type_: AssetType::Zbi,
1137                    size: 1,
1138                    sha256: sha256(1),
1139                    url: test_url("1"),
1140                },
1141                AssetMetadata {
1142                    slot: Slot::Fuchsia,
1143                    type_: AssetType::Vbmeta,
1144                    size: 2,
1145                    sha256: sha256(2),
1146                    url: test_url("2"),
1147                },
1148                AssetMetadata {
1149                    slot: Slot::Recovery,
1150                    type_: AssetType::Zbi,
1151                    size: 3,
1152                    sha256: sha256(3),
1153                    url: test_url("3"),
1154                },
1155                AssetMetadata {
1156                    slot: Slot::Recovery,
1157                    type_: AssetType::Vbmeta,
1158                    size: 4,
1159                    sha256: sha256(4),
1160                    url: test_url("4"),
1161                },
1162            ],
1163            firmware: vec![
1164                FirmwareMetadata {
1165                    type_: "".to_string(),
1166                    size: 5,
1167                    sha256: sha256(5),
1168                    url: test_url("5"),
1169                },
1170                FirmwareMetadata {
1171                    type_: "bl2".to_string(),
1172                    size: 6,
1173                    sha256: sha256(6),
1174                    url: test_url("6"),
1175                },
1176            ],
1177        };
1178
1179        let slots = ImagesMetadata {
1180            fuchsia: Some(ZbiAndOptionalVbmetaMetadata {
1181                zbi: ImageMetadata::new(1, sha256(1), test_url("1")),
1182                vbmeta: Some(ImageMetadata::new(2, sha256(2), test_url("2"))),
1183            }),
1184            recovery: Some(ZbiAndOptionalVbmetaMetadata {
1185                zbi: ImageMetadata::new(3, sha256(3), test_url("3")),
1186                vbmeta: Some(ImageMetadata::new(4, sha256(4), test_url("4"))),
1187            }),
1188            firmware: BTreeMap::from([
1189                ("".into(), ImageMetadata::new(5, sha256(5), test_url("5"))),
1190                ("bl2".into(), ImageMetadata::new(6, sha256(6), test_url("6"))),
1191            ]),
1192        };
1193
1194        let translated_manifest: ImagesMetadata = manifest.into();
1195        assert_eq!(translated_manifest, slots);
1196    }
1197}