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