1use std::sync::Arc;
7
8use anyhow::Result;
9
10use composefs::fsverity::FsVerityHashValue;
11use composefs::repository::Repository;
12
13use crate::OciDigest;
14
15#[cfg(feature = "boot")]
17pub fn generate_boot_image<ObjectID: FsVerityHashValue>(
18 repo: &Arc<Repository<ObjectID>>,
19 manifest_digest: &OciDigest,
20) -> Result<ObjectID> {
21 if let Some(existing) = boot_image(repo, manifest_digest)? {
22 return Ok(existing);
23 }
24
25 let erofs_id = crate::ensure_oci_composefs_erofs_boot(repo, manifest_digest, None, None)?
26 .expect("container image should produce boot EROFS");
27
28 Ok(erofs_id)
29}
30
31pub fn boot_image<ObjectID: FsVerityHashValue>(
33 repo: &Repository<ObjectID>,
34 manifest_digest: &OciDigest,
35) -> Result<Option<ObjectID>> {
36 crate::composefs_boot_erofs_for_manifest(repo, manifest_digest, None, repo.erofs_version())
37}
38
39pub fn remove_boot_image<ObjectID: FsVerityHashValue>(
43 repo: &Arc<Repository<ObjectID>>,
44 manifest_digest: &OciDigest,
45) -> Result<()> {
46 let img = crate::oci_image::OciImage::open(repo, manifest_digest, None)?;
47
48 if !img.is_container_image() {
49 anyhow::bail!("not a container image");
50 }
51
52 if img.boot_image_ref(repo.erofs_version()).is_none() {
53 return Ok(());
54 }
55
56 let config_json = img.read_config_json(repo)?;
58
59 let (_config_digest, new_config_verity) = crate::write_config_raw(
60 repo,
61 &config_json,
62 img.layer_refs().clone(),
63 img.image_ref_v2(), img.image_ref_v1(), None, None, )?;
68
69 let manifest_json = img.read_manifest_json(repo)?;
70 let layer_verities: Vec<_> = img
71 .layer_refs()
72 .iter()
73 .map(|(k, v)| (k.clone(), v.clone()))
74 .collect();
75
76 crate::oci_image::rewrite_manifest(
77 repo,
78 &manifest_json,
79 manifest_digest,
80 &new_config_verity,
81 &layer_verities,
82 None,
83 )?;
84
85 Ok(())
86}
87
88#[cfg(all(test, feature = "boot"))]
89mod test {
90 use super::*;
91 use composefs::fsverity::Sha256HashValue;
92 use composefs::test::TestRepo;
93 use composefs_boot::bootloader::get_boot_resources;
94
95 use crate::oci_image::OciImage;
96 use crate::test_util;
97
98 #[tokio::test]
99 async fn test_boot_image_none_before_generate() {
100 let test_repo = TestRepo::<Sha256HashValue>::new();
101 let repo = &test_repo.repo;
102
103 let img = test_util::create_bootable_image(repo, Some("myapp:v1"), 1).await;
104
105 let result = boot_image(repo, &img.manifest_digest).unwrap();
106 assert!(result.is_none(), "no boot image should exist yet");
107 }
108
109 #[tokio::test]
110 async fn test_generate_boot_image() {
111 let test_repo = TestRepo::<Sha256HashValue>::new();
112 let repo = &test_repo.repo;
113
114 let img = test_util::create_bootable_image(repo, Some("myapp:v1"), 1).await;
115
116 let image_verity = generate_boot_image(repo, &img.manifest_digest).unwrap();
117
118 let found = boot_image(repo, &img.manifest_digest).unwrap();
119 assert_eq!(found, Some(image_verity.clone()));
120
121 let oci = OciImage::open_ref(repo, "myapp:v1").unwrap();
123 assert_eq!(
124 oci.boot_image_ref(repo.erofs_version()),
125 Some(&image_verity)
126 );
127
128 let plain_image = crate::image::create_filesystem(repo, &img.config_digest, None).unwrap();
129 let plain_verity = plain_image.compute_image_id(repo.erofs_version());
130 assert_ne!(
131 image_verity, plain_verity,
132 "boot-transformed image should differ from non-transformed image"
133 );
134 }
135
136 #[tokio::test]
137 async fn test_generate_boot_image_idempotent() {
138 let test_repo = TestRepo::<Sha256HashValue>::new();
139 let repo = &test_repo.repo;
140
141 let img = test_util::create_bootable_image(repo, Some("myapp:v1"), 1).await;
142
143 let v1 = generate_boot_image(repo, &img.manifest_digest).unwrap();
144 let v2 = generate_boot_image(repo, &img.manifest_digest).unwrap();
145 assert_eq!(v1, v2);
146 }
147
148 #[tokio::test]
149 async fn test_remove_boot_image() {
150 let test_repo = TestRepo::<Sha256HashValue>::new();
151 let repo = &test_repo.repo;
152
153 let img = test_util::create_bootable_image(repo, Some("myapp:v1"), 1).await;
154
155 generate_boot_image(repo, &img.manifest_digest).unwrap();
156 assert!(boot_image(repo, &img.manifest_digest).unwrap().is_some());
157
158 remove_boot_image(repo, &img.manifest_digest).unwrap();
159 assert!(
160 boot_image(repo, &img.manifest_digest).unwrap().is_none(),
161 "boot image should be gone after remove"
162 );
163
164 let oci = OciImage::open_ref(repo, "myapp:v1").unwrap();
165 assert!(oci.is_container_image());
166
167 let gc = repo.gc(&[]).unwrap();
168 assert_eq!(
169 gc.images_pruned, 1,
170 "exactly the EROFS image should be pruned"
171 );
172 }
173
174 #[tokio::test]
175 async fn test_remove_boot_image_idempotent() {
176 let test_repo = TestRepo::<Sha256HashValue>::new();
177 let repo = &test_repo.repo;
178
179 let img = test_util::create_bootable_image(repo, Some("myapp:v1"), 1).await;
180
181 remove_boot_image(repo, &img.manifest_digest).unwrap();
182
183 generate_boot_image(repo, &img.manifest_digest).unwrap();
184 remove_boot_image(repo, &img.manifest_digest).unwrap();
185 remove_boot_image(repo, &img.manifest_digest).unwrap();
186
187 assert!(boot_image(repo, &img.manifest_digest).unwrap().is_none());
188 }
189
190 #[tokio::test]
191 async fn test_boot_image_gc_preserves_when_tagged() {
192 let test_repo = TestRepo::<Sha256HashValue>::new();
193 let repo = &test_repo.repo;
194
195 let img = test_util::create_bootable_image(repo, Some("myapp:v1"), 1).await;
196
197 let image_verity = generate_boot_image(repo, &img.manifest_digest).unwrap();
198
199 let gc = repo.gc(&[]).unwrap();
200 assert_eq!(gc.images_pruned, 0);
201 assert_eq!(gc.streams_pruned, 0);
202
203 let oci = OciImage::open_ref(repo, "myapp:v1").unwrap();
204 assert_eq!(
205 oci.boot_image_ref(repo.erofs_version()),
206 Some(&image_verity)
207 );
208 }
209
210 #[tokio::test]
211 async fn test_boot_image_gc_collects_after_untag() {
212 let test_repo = TestRepo::<Sha256HashValue>::new();
213 let repo = &test_repo.repo;
214
215 let img = test_util::create_bootable_image(repo, Some("myapp:v1"), 1).await;
216
217 generate_boot_image(repo, &img.manifest_digest).unwrap();
218
219 crate::oci_image::untag_image(repo, "myapp:v1").unwrap();
220
221 let gc = repo.gc(&[]).unwrap();
222 assert!(gc.objects_removed > 0);
223 assert_eq!(gc.images_pruned, 1);
224 assert!(gc.streams_pruned > 0);
225
226 let gc2 = repo.gc(&[]).unwrap();
227 assert_eq!(gc2.objects_removed, 0);
228 assert_eq!(gc2.images_pruned, 0);
229 assert_eq!(gc2.streams_pruned, 0);
230 }
231
232 #[tokio::test]
233 async fn test_remove_boot_image_then_gc_preserves_oci() {
234 let test_repo = TestRepo::<Sha256HashValue>::new();
235 let repo = &test_repo.repo;
236
237 let img = test_util::create_bootable_image(repo, Some("myapp:v1"), 1).await;
238
239 generate_boot_image(repo, &img.manifest_digest).unwrap();
240
241 remove_boot_image(repo, &img.manifest_digest).unwrap();
242 let gc = repo.gc(&[]).unwrap();
243 assert_eq!(gc.images_pruned, 1);
244
245 let oci = OciImage::open_ref(repo, "myapp:v1").unwrap();
246 assert!(oci.is_container_image());
247 assert!(oci.boot_image_ref(repo.erofs_version()).is_none());
248 }
249
250 #[tokio::test]
252 async fn test_boot_content() {
253 for tag in ["myapp:v1", "uki:v1"] {
254 let test_repo = TestRepo::<Sha256HashValue>::new();
255 let repo = &test_repo.repo;
256
257 let img = test_util::create_bootable_image(repo, Some(tag), 1).await;
258
259 let boot_verity = generate_boot_image(repo, &img.manifest_digest).unwrap();
260
261 let fs = crate::image::create_filesystem(repo, &img.config_digest, None).unwrap();
262 let boot_entries = get_boot_resources(&fs, repo).unwrap();
263 assert_eq!(boot_entries.len(), 2, "tag={tag}");
264 assert!(
265 boot_entries.iter().any(|e| matches!(
266 e,
267 composefs_boot::bootloader::BootEntry::UsrLibModulesVmLinuz(_)
268 )),
269 "tag={tag}: expected vmlinuz entry"
270 );
271 assert!(
272 boot_entries
273 .iter()
274 .any(|e| matches!(e, composefs_boot::bootloader::BootEntry::Type2(_))),
275 "tag={tag}: expected Type2 entry"
276 );
277
278 let plain_fs = crate::image::create_filesystem(repo, &img.config_digest, None).unwrap();
279 let plain_verity = plain_fs.commit_image(repo, None).unwrap();
280 assert_ne!(boot_verity, plain_verity, "tag={tag}");
281 }
282 }
283}