1use std::collections::HashMap;
7use std::sync::Arc;
8
9use anyhow::Result;
10
11use composefs::erofs::format::FormatVersion;
12use composefs::fsverity::FsVerityHashValue;
13#[cfg(feature = "boot")]
14use composefs::generic_tree::OciTransformOptions;
15use composefs::generic_tree::XattrFiltering;
16use composefs::repository::Repository;
17
18use crate::OciDigest;
19#[cfg(feature = "boot")]
20use crate::oci_image::OciImage;
21
22#[cfg(feature = "boot")]
31pub fn generate_boot_image<ObjectID: FsVerityHashValue>(
32 repo: &Arc<Repository<ObjectID>>,
33 manifest_digest: &OciDigest,
34 options: &OciTransformOptions,
35) -> Result<ObjectID> {
36 if let Some(existing) = boot_image_for_mode(repo, manifest_digest, options.xattrs)? {
37 return Ok(existing);
38 }
39
40 let (erofs_id, _) =
41 crate::ensure_oci_composefs_erofs_boot(repo, manifest_digest, None, None, options, false)?
42 .expect("container image should produce boot EROFS");
43
44 Ok(erofs_id)
45}
46
47#[cfg(feature = "boot")]
50pub fn generate_boot_image_get_fs<ObjectID: FsVerityHashValue>(
51 repo: &Arc<Repository<ObjectID>>,
52 manifest_digest: &OciDigest,
53 options: &OciTransformOptions,
54) -> Result<(ObjectID, Option<composefs::tree::FileSystem<ObjectID>>)> {
55 if let Some(existing) = boot_image_for_mode(repo, manifest_digest, options.xattrs)? {
56 return Ok((existing, None));
57 }
58
59 let (erofs_id, untransformed_fs) =
60 crate::ensure_oci_composefs_erofs_boot(repo, manifest_digest, None, None, options, true)?
61 .expect("container image should produce boot EROFS");
62
63 Ok((erofs_id, untransformed_fs))
64}
65
66#[derive(Debug, Clone)]
68pub enum BootImageMatch<ObjectID> {
69 Found {
71 mode: XattrFiltering,
73 version: FormatVersion,
75 digest: ObjectID,
78 },
79 NotFound(usize),
82}
83
84#[cfg(feature = "boot")]
116pub fn find_matching_boot_image<ObjectID: FsVerityHashValue>(
117 repo: &Arc<Repository<ObjectID>>,
118 manifest_digest: &OciDigest,
119 expected: &ObjectID,
120) -> Result<BootImageMatch<ObjectID>> {
121 let mut tried = 0;
122
123 for &mode in XattrFiltering::VARIANTS {
124 match try_mode(repo, manifest_digest, mode, expected)? {
125 ModeResult::Found { version, digest } => {
126 return Ok(BootImageMatch::Found {
127 mode,
128 version,
129 digest,
130 });
131 }
132 ModeResult::NotFound(attempts) => tried += attempts,
133 }
134 }
135
136 Ok(BootImageMatch::NotFound(tried))
137}
138
139#[cfg(feature = "boot")]
142enum ModeResult<ObjectID> {
143 Found {
144 version: FormatVersion,
145 digest: ObjectID,
146 },
147 NotFound(usize),
150}
151
152#[cfg(feature = "boot")]
156fn try_mode<ObjectID: FsVerityHashValue>(
157 repo: &Arc<Repository<ObjectID>>,
158 manifest_digest: &OciDigest,
159 mode: XattrFiltering,
160 expected: &ObjectID,
161) -> Result<ModeResult<ObjectID>> {
162 use composefs_boot::BootOps;
163
164 let img = OciImage::open(repo, manifest_digest, None)?;
165
166 let mut tried = 0;
167 let mut cached_versions = Vec::new();
168
169 for version in FormatVersion::BOOT_VERSIONS {
172 if let Some(digest) = img.boot_image_ref_for_mode(version, mode) {
173 if digest == expected {
174 return Ok(ModeResult::Found {
175 version,
176 digest: digest.clone(),
177 });
178 }
179 tried += 1;
180 cached_versions.push(version);
181 }
182 }
183
184 if cached_versions.len() == FormatVersion::BOOT_VERSIONS.len() {
185 return Ok(ModeResult::NotFound(tried));
188 }
189
190 let options = OciTransformOptions { xattrs: mode };
194 let mut fs = crate::image::create_filesystem(
195 repo,
196 img.config_digest(),
197 Some(img.config_verity()),
198 &options,
199 )?;
200 fs.transform_for_boot(repo)?;
201
202 for version in FormatVersion::BOOT_VERSIONS {
203 if cached_versions.contains(&version) {
204 continue;
205 }
206 let (image_data, digest) = fs.compute_image_bytes(version);
207 if &digest == expected {
208 persist_recovered_boot_image(repo, &img, manifest_digest, mode, version, &image_data)?;
209 return Ok(ModeResult::Found { version, digest });
210 }
211 tried += 1;
212 }
213
214 Ok(ModeResult::NotFound(tried))
215}
216
217#[cfg(feature = "boot")]
227fn persist_recovered_boot_image<ObjectID: FsVerityHashValue>(
228 repo: &Arc<Repository<ObjectID>>,
229 img: &OciImage<ObjectID>,
230 manifest_digest: &OciDigest,
231 mode: XattrFiltering,
232 version: FormatVersion,
233 image_data: &[u8],
234) -> Result<()> {
235 let digest = repo.write_image(None, image_data)?;
236
237 let config_json = img.read_config_json(repo)?;
239
240 let mut boot_images = img.boot_image_refs().clone();
241 boot_images.insert(
242 crate::boot_image_ref_key(version, mode)
243 .into_owned()
244 .into_boxed_str(),
245 digest,
246 );
247
248 let (_config_digest, new_config_verity) = crate::write_config_raw(
249 repo,
250 &config_json,
251 img.layer_refs().clone(),
252 img.image_ref_v2(),
253 img.image_ref_v1(),
254 &boot_images,
255 )?;
256
257 let manifest_json = img.read_manifest_json(repo)?;
258 let layer_verities: Vec<_> = img
259 .layer_refs()
260 .iter()
261 .map(|(k, v)| (k.clone(), v.clone()))
262 .collect();
263
264 crate::oci_image::rewrite_manifest(
265 repo,
266 &manifest_json,
267 manifest_digest,
268 &new_config_verity,
269 &layer_verities,
270 None,
271 )?;
272
273 Ok(())
274}
275
276pub fn boot_image<ObjectID: FsVerityHashValue>(
279 repo: &Repository<ObjectID>,
280 manifest_digest: &OciDigest,
281) -> Result<Option<ObjectID>> {
282 boot_image_for_mode(repo, manifest_digest, XattrFiltering::AllowlistOnly)
283}
284
285pub fn boot_image_for_mode<ObjectID: FsVerityHashValue>(
288 repo: &Repository<ObjectID>,
289 manifest_digest: &OciDigest,
290 mode: XattrFiltering,
291) -> Result<Option<ObjectID>> {
292 crate::composefs_boot_erofs_for_manifest(
293 repo,
294 manifest_digest,
295 None,
296 repo.erofs_version(),
297 mode,
298 )
299}
300
301pub fn remove_boot_image<ObjectID: FsVerityHashValue>(
306 repo: &Arc<Repository<ObjectID>>,
307 manifest_digest: &OciDigest,
308) -> Result<()> {
309 let img = crate::oci_image::OciImage::open(repo, manifest_digest, None)?;
310
311 if !img.is_container_image() {
312 anyhow::bail!("not a container image");
313 }
314
315 if img.boot_image_refs().is_empty() {
316 return Ok(());
317 }
318
319 let config_json = img.read_config_json(repo)?;
321
322 let (_config_digest, new_config_verity) = crate::write_config_raw(
323 repo,
324 &config_json,
325 img.layer_refs().clone(),
326 img.image_ref_v2(), img.image_ref_v1(), &HashMap::new(), )?;
330
331 let manifest_json = img.read_manifest_json(repo)?;
332 let layer_verities: Vec<_> = img
333 .layer_refs()
334 .iter()
335 .map(|(k, v)| (k.clone(), v.clone()))
336 .collect();
337
338 crate::oci_image::rewrite_manifest(
339 repo,
340 &manifest_json,
341 manifest_digest,
342 &new_config_verity,
343 &layer_verities,
344 None,
345 )?;
346
347 Ok(())
348}
349
350#[cfg(all(test, feature = "boot"))]
351mod test {
352 use super::*;
353 use composefs::fsverity::Sha256HashValue;
354 use composefs::test::TestRepo;
355 use composefs_boot::bootloader::get_boot_resources;
356
357 use crate::oci_image::OciImage;
358 use crate::test_util;
359
360 #[tokio::test]
361 async fn test_boot_image_none_before_generate() {
362 let test_repo = TestRepo::<Sha256HashValue>::new();
363 let repo = &test_repo.repo;
364
365 let img = test_util::create_bootable_image(repo, Some("myapp:v1"), 1).await;
366
367 let result = boot_image(repo, &img.manifest_digest).unwrap();
368 assert!(result.is_none(), "no boot image should exist yet");
369 }
370
371 #[tokio::test]
372 async fn test_generate_boot_image() {
373 let test_repo = TestRepo::<Sha256HashValue>::new();
374 let repo = &test_repo.repo;
375
376 let img = test_util::create_bootable_image(repo, Some("myapp:v1"), 1).await;
377
378 let image_verity =
379 generate_boot_image(repo, &img.manifest_digest, &OciTransformOptions::default())
380 .unwrap();
381
382 let found = boot_image(repo, &img.manifest_digest).unwrap();
383 assert_eq!(found, Some(image_verity.clone()));
384
385 let oci = OciImage::open_ref(repo, "myapp:v1").unwrap();
387 assert_eq!(
388 oci.boot_image_ref(repo.erofs_version()),
389 Some(&image_verity)
390 );
391
392 let plain_image = crate::image::create_filesystem(
393 repo,
394 &img.config_digest,
395 None,
396 &OciTransformOptions::default(),
397 )
398 .unwrap();
399 let plain_verity = plain_image.compute_image_id(repo.erofs_version());
400 assert_ne!(
401 image_verity, plain_verity,
402 "boot-transformed image should differ from non-transformed image"
403 );
404 }
405
406 #[tokio::test]
407 async fn test_generate_boot_image_idempotent() {
408 let test_repo = TestRepo::<Sha256HashValue>::new();
409 let repo = &test_repo.repo;
410
411 let img = test_util::create_bootable_image(repo, Some("myapp:v1"), 1).await;
412
413 let v1 = generate_boot_image(repo, &img.manifest_digest, &OciTransformOptions::default())
414 .unwrap();
415 let v2 = generate_boot_image(repo, &img.manifest_digest, &OciTransformOptions::default())
416 .unwrap();
417 assert_eq!(v1, v2);
418 }
419
420 #[tokio::test]
425 async fn test_generate_boot_image_modes_cache_independently() {
426 use test_util::{KernelVersion, OsImage};
427
428 let test_repo = TestRepo::<Sha256HashValue>::new();
429 let repo = &test_repo.repo;
430
431 let img = OsImage::bootable(KernelVersion::V1)
432 .with_layer("/usr/lib/testfile 5 100644 1 0 0 0 0.0 - hello - user.testattr=hi")
433 .build_oci(repo, Some("myapp:v1"))
434 .await;
435
436 let allowlist_id = generate_boot_image(
437 repo,
438 &img.manifest_digest,
439 &OciTransformOptions {
440 xattrs: XattrFiltering::AllowlistOnly,
441 },
442 )
443 .unwrap();
444
445 let keep_user_id = generate_boot_image(
446 repo,
447 &img.manifest_digest,
448 &OciTransformOptions {
449 xattrs: XattrFiltering::KeepUserXattrs,
450 },
451 )
452 .unwrap();
453
454 assert_ne!(
455 allowlist_id, keep_user_id,
456 "KeepUserXattrs should produce a distinct image from AllowlistOnly \
457 when a user.* xattr is present"
458 );
459
460 assert_eq!(
463 boot_image(repo, &img.manifest_digest).unwrap(),
464 Some(allowlist_id.clone()),
465 "AllowlistOnly cache entry should be unaffected by the KeepUserXattrs call"
466 );
467 assert_eq!(
468 boot_image_for_mode(repo, &img.manifest_digest, XattrFiltering::KeepUserXattrs)
469 .unwrap(),
470 Some(keep_user_id.clone())
471 );
472
473 let allowlist_cached =
475 generate_boot_image(repo, &img.manifest_digest, &OciTransformOptions::default())
476 .unwrap();
477 assert_eq!(allowlist_cached, allowlist_id);
478 let keep_user_cached = generate_boot_image(
479 repo,
480 &img.manifest_digest,
481 &OciTransformOptions {
482 xattrs: XattrFiltering::KeepUserXattrs,
483 },
484 )
485 .unwrap();
486 assert_eq!(keep_user_cached, keep_user_id);
487 }
488
489 #[tokio::test]
490 async fn test_remove_boot_image() {
491 let test_repo = TestRepo::<Sha256HashValue>::new();
492 let repo = &test_repo.repo;
493
494 let img = test_util::create_bootable_image(repo, Some("myapp:v1"), 1).await;
495
496 generate_boot_image(repo, &img.manifest_digest, &OciTransformOptions::default()).unwrap();
497 assert!(boot_image(repo, &img.manifest_digest).unwrap().is_some());
498
499 remove_boot_image(repo, &img.manifest_digest).unwrap();
500 assert!(
501 boot_image(repo, &img.manifest_digest).unwrap().is_none(),
502 "boot image should be gone after remove"
503 );
504
505 let oci = OciImage::open_ref(repo, "myapp:v1").unwrap();
506 assert!(oci.is_container_image());
507
508 let gc = repo.gc(&[]).unwrap();
509 assert_eq!(
510 gc.images_pruned, 1,
511 "exactly the EROFS image should be pruned"
512 );
513 }
514
515 #[tokio::test]
516 async fn test_remove_boot_image_idempotent() {
517 let test_repo = TestRepo::<Sha256HashValue>::new();
518 let repo = &test_repo.repo;
519
520 let img = test_util::create_bootable_image(repo, Some("myapp:v1"), 1).await;
521
522 remove_boot_image(repo, &img.manifest_digest).unwrap();
523
524 generate_boot_image(repo, &img.manifest_digest, &OciTransformOptions::default()).unwrap();
525 remove_boot_image(repo, &img.manifest_digest).unwrap();
526 remove_boot_image(repo, &img.manifest_digest).unwrap();
527
528 assert!(boot_image(repo, &img.manifest_digest).unwrap().is_none());
529 }
530
531 #[tokio::test]
532 async fn test_remove_boot_image_clears_all_modes() {
533 let test_repo = TestRepo::<Sha256HashValue>::new();
534 let repo = &test_repo.repo;
535
536 let img = test_util::create_bootable_image(repo, Some("myapp:v1"), 1).await;
537
538 generate_boot_image(
539 repo,
540 &img.manifest_digest,
541 &OciTransformOptions {
542 xattrs: XattrFiltering::AllowlistOnly,
543 },
544 )
545 .unwrap();
546 generate_boot_image(
547 repo,
548 &img.manifest_digest,
549 &OciTransformOptions {
550 xattrs: XattrFiltering::KeepUserXattrs,
551 },
552 )
553 .unwrap();
554
555 remove_boot_image(repo, &img.manifest_digest).unwrap();
556
557 assert!(boot_image(repo, &img.manifest_digest).unwrap().is_none());
558 assert!(
559 boot_image_for_mode(repo, &img.manifest_digest, XattrFiltering::KeepUserXattrs)
560 .unwrap()
561 .is_none()
562 );
563 }
564
565 #[tokio::test]
569 async fn test_boot_image_gc_handles_both_modes_simultaneously() {
570 use test_util::{KernelVersion, OsImage};
571
572 let test_repo = TestRepo::<Sha256HashValue>::new();
573 let repo = &test_repo.repo;
574
575 let img = OsImage::bootable(KernelVersion::V1)
576 .with_layer("/usr/lib/testfile 5 100644 1 0 0 0 0.0 - hello - user.testattr=hi")
577 .build_oci(repo, Some("myapp:v1"))
578 .await;
579
580 let allowlist_id = generate_boot_image(
581 repo,
582 &img.manifest_digest,
583 &OciTransformOptions {
584 xattrs: XattrFiltering::AllowlistOnly,
585 },
586 )
587 .unwrap();
588 let keep_user_id = generate_boot_image(
589 repo,
590 &img.manifest_digest,
591 &OciTransformOptions {
592 xattrs: XattrFiltering::KeepUserXattrs,
593 },
594 )
595 .unwrap();
596 assert_ne!(allowlist_id, keep_user_id);
597
598 let gc = repo.gc(&[]).unwrap();
600 assert_eq!(gc.images_pruned, 0);
601 assert_eq!(gc.streams_pruned, 0);
602
603 let oci = OciImage::open_ref(repo, "myapp:v1").unwrap();
604 assert_eq!(
605 oci.boot_image_ref(repo.erofs_version()),
606 Some(&allowlist_id)
607 );
608 assert_eq!(
609 oci.boot_image_ref_for_mode(repo.erofs_version(), XattrFiltering::KeepUserXattrs),
610 Some(&keep_user_id)
611 );
612
613 crate::oci_image::untag_image(repo, "myapp:v1").unwrap();
615 let gc = repo.gc(&[]).unwrap();
616 assert_eq!(
617 gc.images_pruned, 2,
618 "both the AllowlistOnly and KeepUserXattrs boot images should be pruned"
619 );
620 }
621
622 #[tokio::test]
623 async fn test_boot_image_gc_preserves_when_tagged() {
624 let test_repo = TestRepo::<Sha256HashValue>::new();
625 let repo = &test_repo.repo;
626
627 let img = test_util::create_bootable_image(repo, Some("myapp:v1"), 1).await;
628
629 let image_verity =
630 generate_boot_image(repo, &img.manifest_digest, &OciTransformOptions::default())
631 .unwrap();
632
633 let gc = repo.gc(&[]).unwrap();
634 assert_eq!(gc.images_pruned, 0);
635 assert_eq!(gc.streams_pruned, 0);
636
637 let oci = OciImage::open_ref(repo, "myapp:v1").unwrap();
638 assert_eq!(
639 oci.boot_image_ref(repo.erofs_version()),
640 Some(&image_verity)
641 );
642 }
643
644 #[tokio::test]
645 async fn test_boot_image_gc_collects_after_untag() {
646 let test_repo = TestRepo::<Sha256HashValue>::new();
647 let repo = &test_repo.repo;
648
649 let img = test_util::create_bootable_image(repo, Some("myapp:v1"), 1).await;
650
651 generate_boot_image(repo, &img.manifest_digest, &OciTransformOptions::default()).unwrap();
652
653 crate::oci_image::untag_image(repo, "myapp:v1").unwrap();
654
655 let gc = repo.gc(&[]).unwrap();
656 assert!(gc.objects_removed > 0);
657 assert_eq!(gc.images_pruned, 1);
658 assert!(gc.streams_pruned > 0);
659
660 let gc2 = repo.gc(&[]).unwrap();
661 assert_eq!(gc2.objects_removed, 0);
662 assert_eq!(gc2.images_pruned, 0);
663 assert_eq!(gc2.streams_pruned, 0);
664 }
665
666 #[tokio::test]
667 async fn test_remove_boot_image_then_gc_preserves_oci() {
668 let test_repo = TestRepo::<Sha256HashValue>::new();
669 let repo = &test_repo.repo;
670
671 let img = test_util::create_bootable_image(repo, Some("myapp:v1"), 1).await;
672
673 generate_boot_image(repo, &img.manifest_digest, &OciTransformOptions::default()).unwrap();
674
675 remove_boot_image(repo, &img.manifest_digest).unwrap();
676 let gc = repo.gc(&[]).unwrap();
677 assert_eq!(gc.images_pruned, 1);
678
679 let oci = OciImage::open_ref(repo, "myapp:v1").unwrap();
680 assert!(oci.is_container_image());
681 assert!(oci.boot_image_ref(repo.erofs_version()).is_none());
682 }
683
684 #[tokio::test]
688 async fn test_find_matching_boot_image_default_mode() {
689 let test_repo = TestRepo::<Sha256HashValue>::new();
690 let repo = &test_repo.repo;
691
692 let img = test_util::create_bootable_image(repo, Some("myapp:v1"), 1).await;
693
694 let allowlist_id = generate_boot_image(
695 repo,
696 &img.manifest_digest,
697 &OciTransformOptions {
698 xattrs: XattrFiltering::AllowlistOnly,
699 },
700 )
701 .unwrap();
702
703 let result = find_matching_boot_image(repo, &img.manifest_digest, &allowlist_id).unwrap();
704 assert!(matches!(
705 result,
706 BootImageMatch::Found {
707 mode: XattrFiltering::AllowlistOnly,
708 version: FormatVersion::V1,
709 digest,
710 } if digest == allowlist_id
711 ));
712 }
713
714 #[tokio::test]
718 async fn test_find_matching_boot_image_non_default_mode() {
719 use test_util::{KernelVersion, OsImage};
720
721 let test_repo = TestRepo::<Sha256HashValue>::new();
722 let repo = &test_repo.repo;
723
724 let img = OsImage::bootable(KernelVersion::V1)
725 .with_layer("/usr/lib/testfile 5 100644 1 0 0 0 0.0 - hello - user.testattr=hi")
726 .build_oci(repo, Some("myapp:v1"))
727 .await;
728
729 let keep_user_id = generate_boot_image(
730 repo,
731 &img.manifest_digest,
732 &OciTransformOptions {
733 xattrs: XattrFiltering::KeepUserXattrs,
734 },
735 )
736 .unwrap();
737
738 let result = find_matching_boot_image(repo, &img.manifest_digest, &keep_user_id).unwrap();
739 assert!(matches!(
740 result,
741 BootImageMatch::Found {
742 mode: XattrFiltering::KeepUserXattrs,
743 version: FormatVersion::V1,
744 digest,
745 } if digest == keep_user_id
746 ));
747 }
748
749 #[tokio::test]
759 async fn test_find_matching_boot_image_not_found() {
760 let test_repo = TestRepo::<Sha256HashValue>::new();
761 let repo = &test_repo.repo;
762
763 let img = test_util::create_bootable_image(repo, Some("myapp:v1"), 1).await;
764 let bogus = Sha256HashValue::from_hex("ff".repeat(32)).unwrap();
765
766 let result = find_matching_boot_image(repo, &img.manifest_digest, &bogus).unwrap();
767 let BootImageMatch::NotFound(tried) = result else {
768 panic!("expected NotFound, got a match");
769 };
770 assert_eq!(
771 tried,
772 XattrFiltering::VARIANTS.len() * FormatVersion::BOOT_VERSIONS.len()
773 );
774 }
775
776 #[tokio::test]
788 async fn test_find_matching_boot_image_non_default_version() {
789 use composefs::erofs::format::FormatConfig;
790 use composefs::repository::RepositoryConfig;
791 use composefs_boot::BootOps;
792 use rustix::fs::CWD;
793
794 let dir = composefs::test::tempdir();
795 let repo_path = dir.path().join("repo");
796 let mut config = RepositoryConfig::new(Sha256HashValue::ALGORITHM).set_insecure();
797 config.erofs_formats = FormatConfig::single(FormatVersion::V2);
798 let (repo, _) = Repository::init_path(CWD, &repo_path, config).unwrap();
799 let repo = Arc::new(repo);
800
801 let img = test_util::create_bootable_image(&repo, Some("myapp:v1"), 1).await;
802
803 let v2_id =
806 generate_boot_image(&repo, &img.manifest_digest, &OciTransformOptions::default())
807 .unwrap();
808
809 let mut fs = crate::image::create_filesystem(
813 &repo,
814 &img.config_digest,
815 None,
816 &OciTransformOptions::default(),
817 )
818 .unwrap();
819 fs.transform_for_boot(&repo).unwrap();
820 let v1_id = fs.compute_image_id(FormatVersion::V1);
821 assert_ne!(v1_id, v2_id);
822
823 let oci = OciImage::open(&repo, &img.manifest_digest, None).unwrap();
824 assert!(
825 oci.boot_image_ref_v1().is_none(),
826 "V1 ref should not exist yet in this V2-only repo"
827 );
828
829 let result = find_matching_boot_image(&repo, &img.manifest_digest, &v1_id).unwrap();
830 assert!(matches!(
831 result,
832 BootImageMatch::Found {
833 mode: XattrFiltering::AllowlistOnly,
834 version: FormatVersion::V1,
835 digest,
836 } if digest == v1_id
837 ));
838
839 let oci_after = OciImage::open(&repo, &img.manifest_digest, None).unwrap();
841 assert_eq!(oci_after.boot_image_ref_v1(), Some(&v1_id));
842 }
843
844 #[tokio::test]
846 async fn test_boot_content() {
847 for tag in ["myapp:v1", "uki:v1"] {
848 let test_repo = TestRepo::<Sha256HashValue>::new();
849 let repo = &test_repo.repo;
850
851 let img = test_util::create_bootable_image(repo, Some(tag), 1).await;
852
853 let boot_verity =
854 generate_boot_image(repo, &img.manifest_digest, &OciTransformOptions::default())
855 .unwrap();
856
857 let fs = crate::image::create_filesystem(
858 repo,
859 &img.config_digest,
860 None,
861 &OciTransformOptions::default(),
862 )
863 .unwrap();
864 let boot_entries = get_boot_resources(&fs, repo).unwrap();
865 assert_eq!(boot_entries.len(), 2, "tag={tag}");
866 assert!(
867 boot_entries.iter().any(|e| matches!(
868 e,
869 composefs_boot::bootloader::BootEntry::UsrLibModulesVmLinuz(_)
870 )),
871 "tag={tag}: expected vmlinuz entry"
872 );
873 assert!(
874 boot_entries
875 .iter()
876 .any(|e| matches!(e, composefs_boot::bootloader::BootEntry::Type2(_))),
877 "tag={tag}: expected Type2 entry"
878 );
879
880 let plain_fs = crate::image::create_filesystem(
881 repo,
882 &img.config_digest,
883 None,
884 &OciTransformOptions::default(),
885 )
886 .unwrap();
887 let plain_verity = plain_fs.commit_image(repo, None).unwrap();
888 assert_ne!(boot_verity, plain_verity, "tag={tag}");
889 }
890 }
891}