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 struct WriteHasher<'a, D>(&'a mut D);
307 impl<D: sha2::Digest> std::io::Write for WriteHasher<'_, D> {
308 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
309 self.0.update(buf);
310 Ok(buf.len())
311 }
312 fn flush(&mut self) -> std::io::Result<()> {
313 Ok(())
314 }
315 }
316
317 let mut hasher = sha2::Sha256::new();
318 let file = std::fs::File::open(path).map_err(ImageMetadataError::Io)?;
319 let mut reader = std::io::BufReader::new(file);
320 let size = std::io::copy(&mut reader, &mut WriteHasher(&mut hasher))
321 .map_err(ImageMetadataError::Io)?;
322 let sha256 = fuchsia_hash::Sha256::from(*AsRef::<[u8; 32]>::as_ref(&hasher.finalize()));
323
324 let url = AbsoluteComponentUrl::from_package_url_and_resource(url.into(), resource)
325 .map_err(ImageMetadataError::InvalidResourcePath)?;
326
327 Ok(Self { size, sha256, url })
328 }
329}
330
331impl ImagePackagesManifestBuilder {
332 pub fn fuchsia_package(
335 &mut self,
336 zbi: ImageMetadata,
337 vbmeta: Option<ImageMetadata>,
338 ) -> &mut Self {
339 self.slots.fuchsia = Some(ZbiAndOptionalVbmetaMetadata { zbi, vbmeta });
340 self
341 }
342
343 pub fn recovery_package(
346 &mut self,
347 zbi: ImageMetadata,
348 vbmeta: Option<ImageMetadata>,
349 ) -> &mut Self {
350 self.slots.recovery = Some(ZbiAndOptionalVbmetaMetadata { zbi, vbmeta });
351 self
352 }
353
354 pub fn firmware_package(&mut self, firmware: BTreeMap<String, ImageMetadata>) -> &mut Self {
356 self.slots.firmware = firmware;
357 self
358 }
359
360 pub fn build(self) -> VersionedImagePackagesManifest {
362 let mut assets = vec![];
363 let mut firmware = vec![];
364
365 if let Some(slot) = self.slots.fuchsia {
366 assets.push(AssetMetadata::new_from_metadata(Slot::Fuchsia, AssetType::Zbi, slot.zbi));
367 if let Some(vbmeta) = slot.vbmeta {
368 assets.push(AssetMetadata::new_from_metadata(
369 Slot::Fuchsia,
370 AssetType::Vbmeta,
371 vbmeta,
372 ));
373 }
374 }
375
376 if let Some(slot) = self.slots.recovery {
377 assets.push(AssetMetadata::new_from_metadata(Slot::Recovery, AssetType::Zbi, slot.zbi));
378 if let Some(vbmeta) = slot.vbmeta {
379 assets.push(AssetMetadata::new_from_metadata(
380 Slot::Recovery,
381 AssetType::Vbmeta,
382 vbmeta,
383 ));
384 }
385 }
386
387 for (type_, metadata) in self.slots.firmware {
388 firmware.push(FirmwareMetadata::new_from_metadata(type_, metadata));
389 }
390
391 VersionedImagePackagesManifest::Version1(ImagePackagesManifest { assets, firmware })
392 }
393}
394
395impl ImagesMetadata {
396 pub fn verify(&self, mode: UpdateMode) -> Result<(), VerifyError> {
401 let contains_zbi_entry = self.fuchsia.is_some();
402 match mode {
403 UpdateMode::Normal if !contains_zbi_entry => Err(VerifyError::MissingZbi),
404 UpdateMode::ForceRecovery if contains_zbi_entry => Err(VerifyError::UnexpectedZbi),
405 _ => Ok(()),
406 }
407 }
408
409 pub fn fuchsia(&self) -> Option<&ZbiAndOptionalVbmetaMetadata> {
412 self.fuchsia.as_ref()
413 }
414
415 pub fn recovery(&self) -> Option<&ZbiAndOptionalVbmetaMetadata> {
418 self.recovery.as_ref()
419 }
420
421 pub fn firmware(&self) -> &BTreeMap<String, ImageMetadata> {
424 &self.firmware
425 }
426}
427
428impl<'de> Deserialize<'de> for ImagePackagesManifest {
429 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
430 where
431 D: serde::Deserializer<'de>,
432 {
433 use serde::de::Error;
434
435 #[derive(Debug, Deserialize)]
436 pub struct DeImagePackagesManifest {
437 partitions: Vec<AssetMetadata>,
438 firmware: Vec<FirmwareMetadata>,
439 }
440
441 let parsed = DeImagePackagesManifest::deserialize(deserializer)?;
442
443 {
446 let mut keys = HashSet::new();
447 for image in &parsed.partitions {
448 if image.metadata().url().package_url().hash().is_none() {
449 return Err(D::Error::custom(format!(
450 "image url {:?} does not contain hash",
451 image.metadata().url()
452 )));
453 }
454
455 if !keys.insert(image.key()) {
456 return Err(D::Error::custom(format!(
457 "duplicate image entry: {:?}",
458 image.key()
459 )));
460 }
461 }
462
463 for slot in [Slot::Fuchsia, Slot::Recovery] {
464 if keys.contains(&(slot, AssetType::Vbmeta))
465 && !keys.contains(&(slot, AssetType::Zbi))
466 {
467 return Err(D::Error::custom(format!(
468 "vbmeta without zbi entry in partition {slot:?}"
469 )));
470 }
471 }
472 }
473
474 {
476 let mut keys = HashSet::new();
477 for image in &parsed.firmware {
478 if image.metadata().url().package_url().hash().is_none() {
479 return Err(D::Error::custom(format!(
480 "firmware url {:?} does not contain hash",
481 image.metadata().url()
482 )));
483 }
484
485 if !keys.insert(image.key()) {
486 return Err(D::Error::custom(format!(
487 "duplicate firmware entry: {:?}",
488 image.key()
489 )));
490 }
491 }
492 }
493
494 Ok(ImagePackagesManifest { assets: parsed.partitions, firmware: parsed.firmware })
495 }
496}
497
498pub fn parse_image_packages_json(
500 contents: &[u8],
501) -> Result<ImagePackagesManifest, ImagePackagesError> {
502 let VersionedImagePackagesManifest::Version1(manifest) =
503 serde_json::from_slice(contents).map_err(ImagePackagesError::Parse)?;
504
505 Ok(manifest)
506}
507
508pub(crate) async fn images_metadata(
509 proxy: &fio::DirectoryProxy,
510) -> Result<ImagesMetadata, ImagePackagesError> {
511 image_packages(proxy).await.map(Into::into)
512}
513
514async fn image_packages(
515 proxy: &fio::DirectoryProxy,
516) -> Result<ImagePackagesManifest, ImagePackagesError> {
517 let file = fuchsia_fs::directory::open_file(proxy, "images.json", fio::PERM_READABLE)
518 .await
519 .map_err(|e| match e {
520 fuchsia_fs::node::OpenError::OpenError(Status::NOT_FOUND) => {
521 ImagePackagesError::NotFound
522 }
523 e => ImagePackagesError::Open(e),
524 })?;
525
526 let contents = fuchsia_fs::file::read(&file).await.map_err(ImagePackagesError::Read)?;
527
528 parse_image_packages_json(&contents)
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534 use assert_matches::assert_matches;
535 use serde_json::json;
536 use std::fs::File;
537 use std::io::Write;
538 use vfs::file::vmo::read_only;
539 use vfs::pseudo_directory;
540
541 fn sha256(n: u8) -> fuchsia_hash::Sha256 {
542 [n; 32].into()
543 }
544
545 fn sha256str(n: u8) -> String {
546 sha256(n).to_string()
547 }
548
549 fn hashstr(n: u8) -> String {
550 fuchsia_hash::Hash::from([n; 32]).to_string()
551 }
552
553 fn test_url(data: &str) -> AbsoluteComponentUrl {
554 format!("fuchsia-pkg://fuchsia.com/update-images-firmware/0?hash=000000000000000000000000000000000000000000000000000000000000000a#{data}").parse().unwrap()
555 }
556
557 fn image_package_url(name: &str, hash: u8) -> PinnedAbsolutePackageUrl {
558 format!("fuchsia-pkg://fuchsia.com/{name}/0?hash={}", hashstr(hash)).parse().unwrap()
559 }
560
561 fn image_package_resource_url(name: &str, hash: u8, resource: &str) -> AbsoluteComponentUrl {
562 format!("fuchsia-pkg://fuchsia.com/{name}/0?hash={}#{resource}", hashstr(hash))
563 .parse()
564 .unwrap()
565 }
566
567 #[test]
568 fn image_metadata_for_path_empty() {
569 let tmp = tempfile::tempdir().expect("/tmp to exist");
570 let path = Utf8Path::from_path(tmp.path()).unwrap().join("empty");
571 let mut f = File::create(&path).unwrap();
572 f.write_all(b"").unwrap();
573 drop(f);
574
575 let resource = "resource";
576 let url = image_package_url("package", 1);
577
578 assert_eq!(
579 ImageMetadata::for_path(&path, url, resource.to_string()).unwrap(),
580 ImageMetadata::new(
581 0,
582 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".parse().unwrap(),
583 image_package_resource_url("package", 1, resource)
584 ),
585 );
586 }
587
588 #[test]
589 fn image_metadata_for_path_with_unaligned_data() {
590 let tmp = tempfile::tempdir().expect("/tmp to exist");
591 let path = Utf8Path::from_path(tmp.path()).unwrap().join("empty");
592 let mut f = File::create(&path).unwrap();
593 f.write_all(&[0; 8192 + 4096]).unwrap();
594 drop(f);
595
596 let resource = "resource";
597
598 let url = image_package_url("package", 1);
599
600 assert_eq!(
601 ImageMetadata::for_path(&path, url, resource.to_string()).unwrap(),
602 ImageMetadata::new(
603 8192 + 4096,
604 "f3cc103136423a57975750907ebc1d367e2985ac6338976d4d5a439f50323f4a".parse().unwrap(),
605 image_package_resource_url("package", 1, resource)
606 ),
607 );
608 }
609
610 #[test]
611 fn parses_minimal_manifest() {
612 let raw_json = json!({
613 "version": "1",
614 "contents": { "partitions" : [], "firmware" : []},
615 })
616 .to_string();
617
618 let actual = parse_image_packages_json(raw_json.as_bytes()).unwrap();
619 assert_eq!(actual, ImagePackagesManifest { assets: vec![], firmware: vec![] });
620 }
621
622 #[test]
623 fn builder_builds_minimal() {
624 assert_eq!(
625 ImagePackagesManifest::builder().build(),
626 VersionedImagePackagesManifest::Version1(ImagePackagesManifest {
627 assets: vec![],
628 firmware: vec![],
629 }),
630 );
631 }
632
633 #[test]
634 fn builder_builds_populated_manifest() {
635 let actual = ImagePackagesManifest::builder()
636 .fuchsia_package(
637 ImageMetadata::new(
638 1,
639 sha256(1),
640 image_package_resource_url("update-images-fuchsia", 9, "zbi"),
641 ),
642 Some(ImageMetadata::new(
643 2,
644 sha256(2),
645 image_package_resource_url("update-images-fuchsia", 8, "vbmeta"),
646 )),
647 )
648 .recovery_package(
649 ImageMetadata::new(
650 3,
651 sha256(3),
652 image_package_resource_url("update-images-recovery", 7, "zbi"),
653 ),
654 None,
655 )
656 .firmware_package(BTreeMap::from([
657 (
658 "".into(),
659 ImageMetadata::new(
660 5,
661 sha256(5),
662 image_package_resource_url("update-images-firmware", 6, "a"),
663 ),
664 ),
665 (
666 "bl2".into(),
667 ImageMetadata::new(
668 6,
669 sha256(6),
670 image_package_resource_url("update-images-firmware", 5, "b"),
671 ),
672 ),
673 ]))
674 .clone()
675 .build();
676 assert_eq!(
677 actual,
678 VersionedImagePackagesManifest::Version1(ImagePackagesManifest {
679 assets: vec![
680 AssetMetadata {
681 slot: Slot::Fuchsia,
682 type_: AssetType::Zbi,
683 size: 1,
684 sha256: sha256(1),
685 url: image_package_resource_url("update-images-fuchsia", 9, "zbi"),
686 },
687 AssetMetadata {
688 slot: Slot::Fuchsia,
689 type_: AssetType::Vbmeta,
690 size: 2,
691 sha256: sha256(2),
692 url: image_package_resource_url("update-images-fuchsia", 8, "vbmeta"),
693 },
694 AssetMetadata {
695 slot: Slot::Recovery,
696 type_: AssetType::Zbi,
697 size: 3,
698 sha256: sha256(3),
699 url: image_package_resource_url("update-images-recovery", 7, "zbi"),
700 },
701 ],
702 firmware: vec![
703 FirmwareMetadata {
704 type_: "".to_owned(),
705 size: 5,
706 sha256: sha256(5),
707 url: image_package_resource_url("update-images-firmware", 6, "a"),
708 },
709 FirmwareMetadata {
710 type_: "bl2".to_owned(),
711 size: 6,
712 sha256: sha256(6),
713 url: image_package_resource_url("update-images-firmware", 5, "b"),
714 },
715 ],
716 })
717 );
718 }
719
720 #[test]
721 fn parses_example_manifest() {
722 let raw_json = json!({
723 "version": "1",
724 "contents": {
725 "partitions": [
726 {
727 "slot" : "fuchsia",
728 "type" : "zbi",
729 "size" : 1,
730 "hash" : sha256str(1),
731 "url" : image_package_resource_url("package", 1, "zbi")
732 }, {
733 "slot" : "fuchsia",
734 "type" : "vbmeta",
735 "size" : 2,
736 "hash" : sha256str(2),
737 "url" : image_package_resource_url("package", 1, "vbmeta")
738 },
739 {
740 "slot" : "recovery",
741 "type" : "zbi",
742 "size" : 3,
743 "hash" : sha256str(3),
744 "url" : image_package_resource_url("package", 1, "rzbi")
745 }, {
746 "slot" : "recovery",
747 "type" : "vbmeta",
748 "size" : 3,
749 "hash" : sha256str(3),
750 "url" : image_package_resource_url("package", 1, "rvbmeta")
751 },
752 ],
753 "firmware": [
754 {
755 "type" : "",
756 "size" : 5,
757 "hash" : sha256str(5),
758 "url" : image_package_resource_url("package", 1, "firmware")
759 }, {
760 "type" : "bl2",
761 "size" : 6,
762 "hash" : sha256str(6),
763 "url" : image_package_resource_url("package", 1, "firmware")
764 },
765 ],
766
767 }
768
769 }
770 )
771 .to_string();
772
773 let actual = parse_image_packages_json(raw_json.as_bytes()).unwrap();
774 assert_eq!(
775 ImagesMetadata::from(actual),
776 ImagesMetadata {
777 fuchsia: Some(ZbiAndOptionalVbmetaMetadata {
778 zbi: ImageMetadata::new(
779 1,
780 sha256(1),
781 image_package_resource_url("package", 1, "zbi")
782 ),
783 vbmeta: Some(ImageMetadata::new(
784 2,
785 sha256(2),
786 image_package_resource_url("package", 1, "vbmeta")
787 )),
788 },),
789 recovery: Some(ZbiAndOptionalVbmetaMetadata {
790 zbi: ImageMetadata::new(
791 3,
792 sha256(3),
793 image_package_resource_url("package", 1, "rzbi")
794 ),
795 vbmeta: Some(ImageMetadata::new(
796 3,
797 sha256(3),
798 image_package_resource_url("package", 1, "rvbmeta")
799 )),
800 }),
801 firmware: BTreeMap::from([
802 (
803 "".into(),
804 ImageMetadata::new(
805 5,
806 sha256(5),
807 image_package_resource_url("package", 1, "firmware")
808 )
809 ),
810 (
811 "bl2".into(),
812 ImageMetadata::new(
813 6,
814 sha256(6),
815 image_package_resource_url("package", 1, "firmware")
816 )
817 ),
818 ])
819 }
820 );
821 }
822
823 #[test]
824 fn rejects_duplicate_image_keys() {
825 let raw_json = json!({
826 "version": "1",
827 "contents": {
828 "partitions": [ {
829 "slot" : "fuchsia",
830 "type" : "zbi",
831 "size" : 1,
832 "hash" : sha256str(1),
833 "url" : image_package_resource_url("package", 1, "zbi")
834 }, {
835 "slot" : "fuchsia",
836 "type" : "zbi",
837 "size" : 1,
838 "hash" : sha256str(1),
839 "url" : image_package_resource_url("package", 1, "zbi")
840 },
841 ],
842 "firmware": [],
843 }
844 })
845 .to_string();
846
847 assert_matches!(
848 parse_image_packages_json(raw_json.as_bytes()),
849 Err(ImagePackagesError::Parse(e))
850 if e.to_string().contains("duplicate image entry: (Fuchsia, Zbi)")
851 );
852 }
853
854 #[test]
855 fn rejects_duplicate_firmware_keys() {
856 let raw_json = json!({
857 "version": "1",
858 "contents": {
859 "partitions": [],
860 "firmware": [
861 {
862 "type" : "",
863 "size" : 5,
864 "hash" : sha256str(5),
865 "url" : image_package_resource_url("package", 1, "firmware")
866 }, {
867 "type" : "",
868 "size" : 5,
869 "hash" : sha256str(5),
870 "url" : image_package_resource_url("package", 1, "firmware")
871 },
872 ],
873 }
874 })
875 .to_string();
876
877 assert_matches!(
878 parse_image_packages_json(raw_json.as_bytes()),
879 Err(ImagePackagesError::Parse(e))
880 if e.to_string().contains(r#"duplicate firmware entry: """#)
881 );
882 }
883
884 #[test]
885 fn rejects_vbmeta_without_zbi() {
886 let raw_json = json!({
887 "version": "1",
888 "contents": {
889 "partitions": [{
890 "slot" : "fuchsia",
891 "type" : "vbmeta",
892 "size" : 1,
893 "hash" : sha256str(1),
894 "url" : image_package_resource_url("package", 1, "vbmeta")
895 }],
896 "firmware": [],
897 }
898 })
899 .to_string();
900
901 assert_matches!(
902 parse_image_packages_json(raw_json.as_bytes()),
903 Err(ImagePackagesError::Parse(e))
904 if e.to_string().contains("vbmeta without zbi entry in partition Fuchsia")
905 );
906 }
907
908 #[test]
909 fn rejects_urls_without_hash_partitions() {
910 let raw_json = json!({
911 "version": "1",
912 "contents": {
913 "partitions": [{
914 "slot" : "fuchsia",
915 "type" : "zbi",
916 "size" : 1,
917 "hash" : sha256str(1),
918 "url" : "fuchsia-pkg://fuchsia.com/package/0#zbi"
919 }],
920 "firmware": [],
921 }
922 })
923 .to_string();
924
925 assert_matches!(
926 parse_image_packages_json(raw_json.as_bytes()),
927 Err(ImagePackagesError::Parse(e)) if e.to_string().contains("does not contain hash")
928 );
929 }
930
931 #[test]
932 fn rejects_urls_without_hash_firmware() {
933 let raw_json = json!({
934 "version": "1",
935 "contents": {
936 "partitions": [],
937 "firmware": [{
938 "type" : "",
939 "size" : 5,
940 "hash" : sha256str(5),
941 "url" : "fuchsia-pkg://fuchsia.com/package/0#firmware"
942 }],
943 }
944 })
945 .to_string();
946
947 assert_matches!(
948 parse_image_packages_json(raw_json.as_bytes()),
949 Err(ImagePackagesError::Parse(e)) if e.to_string().contains("does not contain hash")
950 );
951 }
952
953 #[test]
954 fn verify_mode_normal_requires_zbi() {
955 let with_zbi = ImagesMetadata {
956 fuchsia: Some(ZbiAndOptionalVbmetaMetadata {
957 zbi: ImageMetadata::new(1, sha256(1), test_url("zbi")),
958 vbmeta: None,
959 }),
960 recovery: None,
961 firmware: BTreeMap::new(),
962 };
963
964 assert_eq!(with_zbi.verify(UpdateMode::Normal), Ok(()));
965
966 let without_zbi =
967 ImagesMetadata { fuchsia: None, recovery: None, firmware: BTreeMap::new() };
968
969 assert_eq!(without_zbi.verify(UpdateMode::Normal), Err(VerifyError::MissingZbi));
970 }
971
972 #[test]
973 fn verify_mode_force_recovery_requires_no_zbi() {
974 let with_zbi = ImagesMetadata {
975 fuchsia: Some(ZbiAndOptionalVbmetaMetadata {
976 zbi: ImageMetadata::new(1, sha256(1), test_url("zbi")),
977 vbmeta: None,
978 }),
979 recovery: None,
980 firmware: BTreeMap::new(),
981 };
982
983 assert_eq!(with_zbi.verify(UpdateMode::ForceRecovery), Err(VerifyError::UnexpectedZbi));
984
985 let without_zbi =
986 ImagesMetadata { fuchsia: None, recovery: None, firmware: BTreeMap::new() };
987
988 assert_eq!(without_zbi.verify(UpdateMode::ForceRecovery), Ok(()));
989 }
990
991 #[fuchsia_async::run_singlethreaded(test)]
992 async fn image_packages_detects_missing_manifest() {
993 let proxy = vfs::directory::serve_read_only(
994 pseudo_directory! {},
995 vfs::execution_scope::ExecutionScope::new(),
996 );
997
998 assert_matches!(image_packages(&proxy).await, Err(ImagePackagesError::NotFound));
999 }
1000
1001 #[fuchsia_async::run_singlethreaded(test)]
1002 async fn image_packages_detects_invalid_json() {
1003 let proxy = vfs::directory::serve_read_only(
1004 pseudo_directory! {
1005 "images.json" => read_only("not json!"),
1006 },
1007 vfs::execution_scope::ExecutionScope::new(),
1008 );
1009
1010 assert_matches!(image_packages(&proxy).await, Err(ImagePackagesError::Parse(_)));
1011 }
1012
1013 #[fuchsia_async::run_singlethreaded(test)]
1014 async fn image_packages_loads_valid_manifest() {
1015 let proxy = vfs::directory::serve_read_only(
1016 pseudo_directory! {
1017 "images.json" => read_only(r#"{
1018"version": "1",
1019"contents": { "partitions" : [], "firmware" : [] }
1020}"#),
1021 },
1022 vfs::execution_scope::ExecutionScope::new(),
1023 );
1024
1025 assert_eq!(
1026 image_packages(&proxy).await.unwrap(),
1027 ImagePackagesManifest { assets: vec![], firmware: vec![] }
1028 );
1029 }
1030
1031 #[fuchsia::test]
1032 fn boot_slot_accessors() {
1033 let slot = ZbiAndOptionalVbmetaMetadata {
1034 zbi: ImageMetadata::new(1, sha256(1), test_url("zbi")),
1035 vbmeta: Some(ImageMetadata::new(2, sha256(2), test_url("vbmeta"))),
1036 };
1037
1038 assert_eq!(slot.zbi(), &ImageMetadata::new(1, sha256(1), test_url("zbi")));
1039 assert_eq!(slot.vbmeta(), Some(&ImageMetadata::new(2, sha256(2), test_url("vbmeta"))));
1040
1041 let slot = ZbiAndOptionalVbmetaMetadata {
1042 zbi: ImageMetadata::new(1, sha256(1), test_url("zbi")),
1043 vbmeta: None,
1044 };
1045 assert_eq!(slot.vbmeta(), None);
1046 }
1047
1048 #[fuchsia::test]
1049 fn image_packages_manifest_accessors() {
1050 let slot = ZbiAndOptionalVbmetaMetadata {
1051 zbi: ImageMetadata::new(1, sha256(1), test_url("zbi")),
1052 vbmeta: Some(ImageMetadata::new(2, sha256(2), test_url("vbmeta"))),
1053 };
1054
1055 let mut builder = ImagePackagesManifest::builder();
1056 builder.fuchsia_package(
1057 ImageMetadata::new(1, sha256(1), test_url("zbi")),
1058 Some(ImageMetadata::new(2, sha256(2), test_url("vbmeta"))),
1059 );
1060 let VersionedImagePackagesManifest::Version1(manifest) = builder.build();
1061
1062 assert_eq!(manifest.fuchsia(), Some(slot.clone()));
1063 assert_eq!(manifest.recovery(), None);
1064 assert_eq!(manifest.firmware(), BTreeMap::new());
1065
1066 let mut builder = ImagePackagesManifest::builder();
1067 builder.recovery_package(
1068 ImageMetadata::new(1, sha256(1), test_url("zbi")),
1069 Some(ImageMetadata::new(2, sha256(2), test_url("vbmeta"))),
1070 );
1071 let VersionedImagePackagesManifest::Version1(manifest) = builder.build();
1072
1073 assert_eq!(manifest.fuchsia(), None);
1074 assert_eq!(manifest.recovery(), Some(slot));
1075 assert_eq!(manifest.firmware(), BTreeMap::new());
1076
1077 let mut builder = ImagePackagesManifest::builder();
1078 builder.firmware_package(BTreeMap::from([(
1079 "".into(),
1080 ImageMetadata::new(
1081 5,
1082 sha256(5),
1083 image_package_resource_url("update-images-firmware", 6, "a"),
1084 ),
1085 )]));
1086 let VersionedImagePackagesManifest::Version1(manifest) = builder.build();
1087 assert_eq!(manifest.fuchsia(), None);
1088 assert_eq!(manifest.recovery(), None);
1089 assert_eq!(
1090 manifest.firmware(),
1091 BTreeMap::from([(
1092 "".into(),
1093 ImageMetadata::new(
1094 5,
1095 sha256(5),
1096 image_package_resource_url("update-images-firmware", 6, "a")
1097 )
1098 )])
1099 )
1100 }
1101
1102 #[fuchsia::test]
1103 fn firmware_image_format_to_image_metadata() {
1104 let assembly_firmware = FirmwareMetadata {
1105 type_: "".to_string(),
1106 size: 1,
1107 sha256: sha256(1),
1108 url: image_package_resource_url("package", 1, "firmware"),
1109 };
1110
1111 let image_meta_data = ImageMetadata {
1112 size: 1,
1113 sha256: sha256(1),
1114 url: image_package_resource_url("package", 1, "firmware"),
1115 };
1116
1117 let firmware_into: ImageMetadata = assembly_firmware.metadata();
1118
1119 assert_eq!(firmware_into, image_meta_data);
1120 }
1121
1122 #[fuchsia::test]
1123 fn assembly_image_format_to_image_metadata() {
1124 let assembly_image = AssetMetadata {
1125 slot: Slot::Fuchsia,
1126 type_: AssetType::Zbi,
1127 size: 1,
1128 sha256: sha256(1),
1129 url: image_package_resource_url("package", 1, "image"),
1130 };
1131
1132 let image_meta_data = ImageMetadata {
1133 size: 1,
1134 sha256: sha256(1),
1135 url: image_package_resource_url("package", 1, "image"),
1136 };
1137
1138 let image_into: ImageMetadata = assembly_image.metadata();
1139
1140 assert_eq!(image_into, image_meta_data);
1141 }
1142
1143 #[fuchsia::test]
1144 fn manifest_conversion_minimal() {
1145 let manifest = ImagePackagesManifest { assets: vec![], firmware: vec![] };
1146
1147 let slots = ImagesMetadata { fuchsia: None, recovery: None, firmware: BTreeMap::new() };
1148
1149 let translated_manifest: ImagesMetadata = manifest.into();
1150 assert_eq!(translated_manifest, slots);
1151 }
1152
1153 #[fuchsia::test]
1154 fn manifest_conversion_maximal() {
1155 let manifest = ImagePackagesManifest {
1156 assets: vec![
1157 AssetMetadata {
1158 slot: Slot::Fuchsia,
1159 type_: AssetType::Zbi,
1160 size: 1,
1161 sha256: sha256(1),
1162 url: test_url("1"),
1163 },
1164 AssetMetadata {
1165 slot: Slot::Fuchsia,
1166 type_: AssetType::Vbmeta,
1167 size: 2,
1168 sha256: sha256(2),
1169 url: test_url("2"),
1170 },
1171 AssetMetadata {
1172 slot: Slot::Recovery,
1173 type_: AssetType::Zbi,
1174 size: 3,
1175 sha256: sha256(3),
1176 url: test_url("3"),
1177 },
1178 AssetMetadata {
1179 slot: Slot::Recovery,
1180 type_: AssetType::Vbmeta,
1181 size: 4,
1182 sha256: sha256(4),
1183 url: test_url("4"),
1184 },
1185 ],
1186 firmware: vec![
1187 FirmwareMetadata {
1188 type_: "".to_string(),
1189 size: 5,
1190 sha256: sha256(5),
1191 url: test_url("5"),
1192 },
1193 FirmwareMetadata {
1194 type_: "bl2".to_string(),
1195 size: 6,
1196 sha256: sha256(6),
1197 url: test_url("6"),
1198 },
1199 ],
1200 };
1201
1202 let slots = ImagesMetadata {
1203 fuchsia: Some(ZbiAndOptionalVbmetaMetadata {
1204 zbi: ImageMetadata::new(1, sha256(1), test_url("1")),
1205 vbmeta: Some(ImageMetadata::new(2, sha256(2), test_url("2"))),
1206 }),
1207 recovery: Some(ZbiAndOptionalVbmetaMetadata {
1208 zbi: ImageMetadata::new(3, sha256(3), test_url("3")),
1209 vbmeta: Some(ImageMetadata::new(4, sha256(4), test_url("4"))),
1210 }),
1211 firmware: BTreeMap::from([
1212 ("".into(), ImageMetadata::new(5, sha256(5), test_url("5"))),
1213 ("bl2".into(), ImageMetadata::new(6, sha256(6), test_url("6"))),
1214 ]),
1215 };
1216
1217 let translated_manifest: ImagesMetadata = manifest.into();
1218 assert_eq!(translated_manifest, slots);
1219 }
1220}