1use 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#[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#[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#[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#[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#[derive(Debug, Clone)]
65pub struct ImagePackagesManifestBuilder {
66 slots: ImagesMetadata,
67}
68
69#[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#[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#[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#[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#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Hash)]
118#[serde(rename_all = "lowercase")]
119#[allow(missing_docs)]
120pub enum Slot {
121 Fuchsia,
124
125 Recovery,
127}
128
129#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Hash)]
131#[serde(rename_all = "lowercase")]
132#[allow(missing_docs)]
133pub enum AssetType {
134 Zbi,
136
137 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#[derive(Debug, PartialEq, Eq, Clone)]
154pub struct ImagesMetadata {
155 fuchsia: Option<ZbiAndOptionalVbmetaMetadata>,
156 recovery: Option<ZbiAndOptionalVbmetaMetadata>,
157 firmware: BTreeMap<String, ImageMetadata>,
158}
159
160#[derive(Debug, PartialEq, Eq, Clone)]
162pub struct ZbiAndOptionalVbmetaMetadata {
163 zbi: ImageMetadata,
165
166 vbmeta: Option<ImageMetadata>,
168}
169
170impl ZbiAndOptionalVbmetaMetadata {
171 pub fn zbi(&self) -> &ImageMetadata {
173 &self.zbi
174 }
175
176 pub fn vbmeta(&self) -> Option<&ImageMetadata> {
178 self.vbmeta.as_ref()
179 }
180}
181
182#[derive(Debug, PartialEq, Eq, Clone)]
185pub struct ImageMetadata {
186 size: u64,
188
189 sha256: fuchsia_hash::Sha256,
192
193 url: AbsoluteComponentUrl,
195}
196
197impl FirmwareMetadata {
198 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 pub fn metadata(&self) -> ImageMetadata {
214 ImageMetadata { size: self.size, sha256: self.sha256, url: self.url.clone() }
215 }
216}
217
218impl AssetMetadata {
219 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 pub fn metadata(&self) -> ImageMetadata {
230 ImageMetadata { size: self.size, sha256: self.sha256, url: self.url.clone() }
231 }
232}
233
234impl ImagePackagesManifest {
235 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 pub fn fuchsia(&self) -> Option<ZbiAndOptionalVbmetaMetadata> {
259 self.slot_metadata(Slot::Fuchsia)
260 }
261
262 pub fn recovery(&self) -> Option<ZbiAndOptionalVbmetaMetadata> {
264 self.slot_metadata(Slot::Recovery)
265 }
266
267 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 pub fn new(size: u64, sha256: fuchsia_hash::Sha256, url: AbsoluteComponentUrl) -> Self {
277 Self { size, sha256, url }
278 }
279
280 pub fn size(&self) -> u64 {
282 self.size
283 }
284
285 pub fn sha256(&self) -> fuchsia_hash::Sha256 {
287 self.sha256
288 }
289
290 pub fn url(&self) -> &AbsoluteComponentUrl {
292 &self.url
293 }
294
295 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 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 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 pub fn firmware_package(&mut self, firmware: BTreeMap<String, ImageMetadata>) -> &mut Self {
341 self.slots.firmware = firmware;
342 self
343 }
344
345 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 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 pub fn fuchsia(&self) -> Option<&ZbiAndOptionalVbmetaMetadata> {
397 self.fuchsia.as_ref()
398 }
399
400 pub fn recovery(&self) -> Option<&ZbiAndOptionalVbmetaMetadata> {
403 self.recovery.as_ref()
404 }
405
406 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 {
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 {
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
483pub 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}