1use 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#[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#[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#[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#[derive(Debug, Clone)]
50pub struct ImagePackagesManifestBuilder {
51 slots: ImagesMetadata,
52}
53
54#[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#[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#[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#[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#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Hash)]
103#[serde(rename_all = "lowercase")]
104#[allow(missing_docs)]
105pub enum Slot {
106 Fuchsia,
109
110 Recovery,
112}
113
114#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Hash)]
116#[serde(rename_all = "lowercase")]
117#[allow(missing_docs)]
118pub enum AssetType {
119 Zbi,
121
122 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#[derive(Debug, PartialEq, Eq, Clone)]
139pub struct ImagesMetadata {
140 fuchsia: Option<ZbiAndOptionalVbmetaMetadata>,
141 recovery: Option<ZbiAndOptionalVbmetaMetadata>,
142 firmware: BTreeMap<String, ImageMetadata>,
143}
144
145#[derive(Debug, PartialEq, Eq, Clone)]
147pub struct ZbiAndOptionalVbmetaMetadata {
148 zbi: ImageMetadata,
150
151 vbmeta: Option<ImageMetadata>,
153}
154
155impl ZbiAndOptionalVbmetaMetadata {
156 pub fn zbi(&self) -> &ImageMetadata {
158 &self.zbi
159 }
160
161 pub fn vbmeta(&self) -> Option<&ImageMetadata> {
163 self.vbmeta.as_ref()
164 }
165}
166
167#[derive(Debug, PartialEq, Eq, Clone)]
170pub struct ImageMetadata {
171 size: u64,
173
174 sha256: fuchsia_hash::Sha256,
177
178 url: AbsoluteComponentUrl,
180}
181
182impl FirmwareMetadata {
183 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 pub fn metadata(&self) -> ImageMetadata {
199 ImageMetadata { size: self.size, sha256: self.sha256, url: self.url.clone() }
200 }
201}
202
203impl AssetMetadata {
204 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 pub fn metadata(&self) -> ImageMetadata {
215 ImageMetadata { size: self.size, sha256: self.sha256, url: self.url.clone() }
216 }
217}
218
219impl ImagePackagesManifest {
220 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 pub fn fuchsia(&self) -> Option<ZbiAndOptionalVbmetaMetadata> {
244 self.slot_metadata(Slot::Fuchsia)
245 }
246
247 pub fn recovery(&self) -> Option<ZbiAndOptionalVbmetaMetadata> {
249 self.slot_metadata(Slot::Recovery)
250 }
251
252 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 pub fn new(size: u64, sha256: fuchsia_hash::Sha256, url: AbsoluteComponentUrl) -> Self {
262 Self { size, sha256, url }
263 }
264
265 pub fn size(&self) -> u64 {
267 self.size
268 }
269
270 pub fn sha256(&self) -> fuchsia_hash::Sha256 {
272 self.sha256
273 }
274
275 pub fn url(&self) -> &AbsoluteComponentUrl {
277 &self.url
278 }
279
280 pub fn for_path(
283 path: &Utf8Path,
284 url: PinnedAbsolutePackageUrl,
285 resource: String,
286 ) -> Result<Self, ImageMetadataError> {
287 use sha2::Digest as _;
288
289 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 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 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 pub fn firmware_package(&mut self, firmware: BTreeMap<String, ImageMetadata>) -> &mut Self {
340 self.slots.firmware = firmware;
341 self
342 }
343
344 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 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 pub fn fuchsia(&self) -> Option<&ZbiAndOptionalVbmetaMetadata> {
396 self.fuchsia.as_ref()
397 }
398
399 pub fn recovery(&self) -> Option<&ZbiAndOptionalVbmetaMetadata> {
402 self.recovery.as_ref()
403 }
404
405 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 {
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 {
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
482pub 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}