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