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