1use std::io::{Read, Seek, SeekFrom};
8use std::path::Path;
9
10use anyhow::{Context, Result};
11use camino::Utf8PathBuf;
12use cap_std_ext::cap_std::fs::Dir;
13use cap_std_ext::dirext::CapStdExtDirExt;
14use composefs_boot::{
15 android_boot::{AndroidBootImage, Component},
16 bootloader::{AbootEncoding, EFI_ADDON_FILE_EXT},
17 cmdline::{KARG_COMPOSEFS_DIGEST, KARG_V2},
18};
19use composefs_ctl::composefs_boot;
20use linux_kernel_cmdline::utf8::Cmdline;
21use ostree_ext::composefs_boot::bootloader::EFI_ADDON_DIR_EXT;
22use serde::Serialize;
23
24use crate::bootc_composefs::boot::{EFI_LINUX, GLOBAL_UKI_ADDONS_DIR};
25use crate::install::UkiAddonOpts;
26
27#[derive(Debug, Serialize)]
29#[serde(rename_all = "kebab-case")]
30pub(crate) struct Kernel {
31 pub(crate) version: String,
35 pub(crate) unified: bool,
37}
38
39pub(crate) enum KernelType {
45 Aboot {
46 path: Utf8PathBuf,
47 encoding: AbootEncoding,
48 cmdline: Cmdline<'static>,
49 },
50 Uki {
51 path: Utf8PathBuf,
52 cmdline: Option<Cmdline<'static>>,
55 },
56 Vmlinuz {
57 path: Utf8PathBuf,
58 initramfs: Utf8PathBuf,
59 },
60}
61
62pub(crate) struct KernelInternal {
69 pub(crate) kernel: Kernel,
70 pub(crate) k_type: KernelType,
71}
72
73impl From<KernelInternal> for Kernel {
74 fn from(kernel_internal: KernelInternal) -> Self {
75 kernel_internal.kernel
76 }
77}
78
79pub(crate) fn find_kernel(
88 root: &Dir,
89 uki_addon_opts: Option<&UkiAddonOpts>,
90) -> Result<Option<KernelInternal>> {
91 if let Some(aboot) = find_aboot_kernel(root)? {
92 anyhow::ensure!(
93 find_uki_path(root)?.is_none()
94 && ostree_ext::bootabletree::find_kernel_dir_fs(root)?.is_none()
95 && !has_type1_entries(root)?,
96 "aboot payload cannot be combined with other boot artifacts"
97 );
98 return Ok(Some(aboot));
99 }
100
101 if let Some(uki_path) = find_uki_path(root)? {
103 let version = uki_path.file_stem().unwrap_or(uki_path.as_str()).to_owned();
104
105 let mut uki = root.open(&uki_path).context("Opening UKI")?;
106
107 let cmdline = composefs_boot::uki::get_section_buffered(&mut uki, ".cmdline");
109
110 let mut cmdline = match cmdline {
111 Ok(cmdline) => {
112 let cmdline_str = std::str::from_utf8(&cmdline)?;
113 Cmdline::from(cmdline_str.to_owned())
114 }
115
116 Err(uki_error) => match uki_error {
117 composefs_boot::uki::UkiError::MissingSection(_) => Cmdline::new(),
118 e => anyhow::bail!("Failed to read UKI cmdline: {e:?}"),
119 },
120 };
121
122 if let Some(uki_addon_opts) = uki_addon_opts {
123 let boot = root.open_dir(crate::install::BOOT)?;
125 validate_uki_addon_cmdlines(&boot, uki_addon_opts, &mut cmdline)?;
126 };
127
128 return Ok(Some(KernelInternal {
129 kernel: Kernel {
130 version,
131 unified: true,
132 },
133 k_type: KernelType::Uki {
134 path: uki_path,
135 cmdline: Some(cmdline),
136 },
137 }));
138 }
139
140 if let Some(modules_dir) = ostree_ext::bootabletree::find_kernel_dir_fs(root)? {
142 let version = modules_dir
143 .file_name()
144 .ok_or_else(|| anyhow::anyhow!("kernel dir should have a file name: {modules_dir}"))?
145 .to_owned();
146 let vmlinuz = modules_dir.join("vmlinuz");
147 let initramfs = modules_dir.join("initramfs.img");
148 return Ok(Some(KernelInternal {
149 kernel: Kernel {
150 version,
151 unified: false,
152 },
153 k_type: KernelType::Vmlinuz {
154 path: vmlinuz,
155 initramfs,
156 },
157 }));
158 }
159
160 Ok(None)
161}
162
163pub(crate) fn read_aboot_metadata<R: Read + Seek>(
164 image: &mut R,
165) -> Result<(AbootEncoding, Cmdline<'static>)> {
166 let mut magic = [0; 8];
167 image
168 .read_exact(&mut magic)
169 .context("Reading aboot payload magic")?;
170 image.seek(SeekFrom::Start(0))?;
171 let (encoding, cmdline) = if magic == *b"ANDROID!" {
172 let header = AndroidBootImage::parse(image).context("Parsing Android boot image")?;
173 header.component(image, Component::Kernel)?;
174 header.component(image, Component::Ramdisk)?;
175 (AbootEncoding::AndroidV2, header.cmdline()?.to_owned())
176 } else {
177 composefs_boot::uki::get_section_buffered(image, ".linux")?;
178 image.seek(SeekFrom::Start(0))?;
179 composefs_boot::uki::get_section_buffered(image, ".initrd")?;
180 image.seek(SeekFrom::Start(0))?;
181 (
182 AbootEncoding::Uki,
183 composefs_boot::uki::get_cmdline_buffered(image)?,
184 )
185 };
186 Ok((encoding, Cmdline::from(cmdline)))
187}
188
189fn find_aboot_kernel(root: &Dir) -> Result<Option<KernelInternal>> {
190 let Some(boot) = root.open_dir_optional("boot")? else {
191 return Ok(None);
192 };
193 let mut payload = None;
194 let mut vbmeta_versions = Vec::new();
195 for entry in boot.entries()? {
196 let entry = entry?;
197 let name = entry.file_name();
198 let Some(name) = name.to_str() else { continue };
199 if let Some(version) = name
200 .strip_prefix("vbmeta-")
201 .and_then(|s| s.strip_suffix(".img"))
202 {
203 anyhow::ensure!(entry.file_type()?.is_file(), "{name} is not a regular file");
204 vbmeta_versions.push(version.to_owned());
205 }
206 let Some(version) = name
207 .strip_prefix("aboot-")
208 .and_then(|s| s.strip_suffix(".img"))
209 else {
210 continue;
211 };
212 anyhow::ensure!(!version.is_empty(), "aboot payload has no kernel version");
213 anyhow::ensure!(entry.file_type()?.is_file(), "{name} is not a regular file");
214 anyhow::ensure!(payload.is_none(), "multiple aboot payloads found");
215 payload = Some((version.to_owned(), name.to_owned()));
216 }
217 for version in &vbmeta_versions {
218 anyhow::ensure!(
219 payload.as_ref().is_some_and(|(v, _)| v == version),
220 "vbmeta image has no matching aboot payload"
221 );
222 }
223 let Some((version, name)) = payload else {
224 return Ok(None);
225 };
226 let path = Utf8PathBuf::from("boot").join(name);
227 let mut file = root
228 .open(&path)
229 .with_context(|| format!("Opening {path}"))?;
230 let (encoding, cmdline) =
231 read_aboot_metadata(&mut file).with_context(|| format!("Parsing {path}"))?;
232 anyhow::ensure!(
233 encoding != AbootEncoding::Uki || vbmeta_versions.is_empty(),
234 "ukiboot payload must not have a vbmeta image"
235 );
236 Ok(Some(KernelInternal {
237 kernel: Kernel {
238 version,
239 unified: true,
240 },
241 k_type: KernelType::Aboot {
242 path,
243 encoding,
244 cmdline,
245 },
246 }))
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
251#[serde(rename_all = "kebab-case")]
252pub(crate) enum ContainerImageType {
253 Aboot,
254 AbootEfi,
255 Uki,
256 Vmlinuz,
257}
258
259impl ContainerImageType {
260 pub(crate) fn as_str(self) -> &'static str {
261 match self {
262 Self::Aboot => "aboot",
263 Self::AbootEfi => "aboot-efi",
264 Self::Uki => "UKI",
265 Self::Vmlinuz => "vmlinuz",
266 }
267 }
268}
269
270impl KernelType {
271 pub(crate) fn cmdline(&self) -> Option<&Cmdline<'static>> {
272 match self {
273 Self::Aboot { cmdline, .. } => Some(cmdline),
274 Self::Uki { cmdline, .. } => cmdline.as_ref(),
275 Self::Vmlinuz { .. } => None,
276 }
277 }
278
279 pub(crate) fn is_unified(&self) -> bool {
280 matches!(self, Self::Uki { .. } | Self::Aboot { .. })
281 }
282
283 pub(crate) fn image_type(&self) -> ContainerImageType {
284 match self {
285 Self::Aboot {
286 encoding: AbootEncoding::AndroidV2,
287 ..
288 } => ContainerImageType::Aboot,
289 Self::Aboot {
290 encoding: AbootEncoding::Uki,
291 ..
292 } => ContainerImageType::AbootEfi,
293 Self::Uki { .. } => ContainerImageType::Uki,
294 Self::Vmlinuz { .. } => ContainerImageType::Vmlinuz,
295 }
296 }
297}
298
299pub(crate) const KERNEL_COMPANION_FILES: &[&str] = &[".vmlinuz.hmac"];
313
314pub(crate) fn split_kernel(root: &Dir, output: &Dir) -> Result<String> {
320 let kernel =
321 find_kernel(root, None)?.ok_or_else(|| anyhow::anyhow!("No kernel found in rootfs"))?;
322 let KernelType::Vmlinuz { path, initramfs } = &kernel.k_type else {
323 anyhow::bail!("Only traditional kernels can be split");
324 };
325 let kver = kernel.kernel.version;
326
327 output
328 .create_dir_all(&kver)
329 .with_context(|| format!("Creating {kver} in output directory"))?;
330 let dest = output.open_dir(&kver)?;
331
332 root.rename(path, &dest, "vmlinuz")
333 .with_context(|| format!("Moving {path}"))?;
334 root.rename(initramfs, &dest, "initramfs.img")
335 .with_context(|| format!("Moving {initramfs}"))?;
336
337 for &name in KERNEL_COMPANION_FILES {
338 let src = path.with_file_name(name);
339 match root.rename(&src, &dest, name) {
340 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
341 r => r.with_context(|| format!("Moving {src}"))?,
342 }
343 }
344
345 Ok(kver)
346}
347
348fn has_type1_entries(root: &Dir) -> Result<bool> {
349 let Some(entries) = root.open_dir_optional("boot/loader/entries")? else {
350 return Ok(false);
351 };
352 for entry in entries.entries()? {
353 if Path::new(&entry?.file_name())
354 .extension()
355 .is_some_and(|s| s == "conf")
356 {
357 return Ok(true);
358 }
359 }
360 Ok(false)
361}
362
363fn find_uki_path(root: &Dir) -> Result<Option<Utf8PathBuf>> {
368 let Some(boot) = root.open_dir_optional(crate::install::BOOT)? else {
369 return Ok(None);
370 };
371 let Some(efi_linux) = boot.open_dir_optional(EFI_LINUX)? else {
372 return Ok(None);
373 };
374
375 let mut uki_files = Vec::new();
376 for entry in efi_linux.entries()? {
377 let entry = entry?;
378 let name = entry.file_name();
379 let name_path = Path::new(&name);
380 let extension = name_path.extension().and_then(|v| v.to_str());
381 if extension == Some("efi") && !name.to_string_lossy().ends_with(".addon.efi") {
382 if let Some(name_str) = name.to_str() {
383 uki_files.push(name_str.to_owned());
384 }
385 }
386 }
387
388 uki_files.sort();
390 Ok(uki_files
391 .into_iter()
392 .next()
393 .map(|filename| Utf8PathBuf::from(format!("boot/{EFI_LINUX}/{filename}"))))
394}
395
396fn cmdline_has_composefs(cmdline: &str) -> bool {
397 Cmdline::from(cmdline).iter().any(|parameter| {
398 let key = &*parameter.key();
399 key == KARG_V2 || key == KARG_COMPOSEFS_DIGEST
400 })
401}
402
403#[fn_error_context::context("Getting cmdline from addon {name}")]
404fn get_cmdline_in_addon<'a>(dir: &'a Dir, name: &'a str) -> Result<Option<Cmdline<'static>>> {
405 let mut file = dir.open(name).with_context(|| format!("Opening {name}"))?;
406
407 match composefs_boot::uki::get_cmdline_buffered(&mut file) {
408 Ok(cmdline) => Ok(Some(Cmdline::from(cmdline))),
409 Err(composefs_boot::uki::UkiError::MissingSection(_)) => Ok(None),
410 Err(e) => Err(e).with_context(|| format!("Reading cmdline from {name}")),
411 }
412}
413
414#[fn_error_context::context("Validating UKI Addon Cmdlines")]
418fn validate_uki_addon_cmdlines(
419 boot: &Dir,
420 uki_addon_opts: &UkiAddonOpts,
421 final_cmdline: &mut Cmdline,
422) -> Result<()> {
423 let selected_global = uki_addon_opts.global.as_deref().unwrap_or(&[]);
424 let selected_scoped = uki_addon_opts.scoped.as_deref().unwrap_or(&[]);
425
426 if let Some(addons_dir) = boot.open_dir_optional(GLOBAL_UKI_ADDONS_DIR)? {
427 for ent in addons_dir.entries_utf8()? {
428 let filename = ent?.file_name()?;
429
430 let Some(name) = filename.strip_suffix(EFI_ADDON_FILE_EXT) else {
431 continue;
432 };
433
434 if !selected_global.iter().any(|n| n == name) {
435 continue;
437 }
438
439 if let Some(addon_cmdline) = get_cmdline_in_addon(&addons_dir, &filename)? {
440 if cmdline_has_composefs(&addon_cmdline) {
441 anyhow::bail!("composefs cmdline found in global UKI addon {name}");
442 }
443
444 final_cmdline.extend(&addon_cmdline);
445 }
446 }
447 };
448
449 let efi_linux = boot
450 .open_dir(EFI_LINUX)
451 .with_context(|| format!("Opening {EFI_LINUX}"))?;
452
453 for ent in efi_linux.entries_utf8()? {
454 let ent = ent.context("Reading directory entry")?;
455 let filename = ent.file_name().context("Reading filename")?;
456 let file_type = ent
457 .file_type()
458 .with_context(|| format!("Getting file type for {filename}"))?;
459
460 if !(filename.ends_with(EFI_ADDON_DIR_EXT) && file_type.is_dir()) {
461 continue;
462 }
463
464 let addon_dir = efi_linux
465 .open_dir(&filename)
466 .with_context(|| format!("Opening directory {filename}"))?;
467
468 for addon_dir_ent in addon_dir
469 .entries_utf8()
470 .with_context(|| format!("Reading dirents {filename}"))?
471 {
472 let addon_entry = addon_dir_ent?;
473 let addon_name = addon_entry.file_name().context("Reading UKI Addon name")?;
474
475 let Some(addon_name_wo_suffix) = addon_name.strip_suffix(EFI_ADDON_FILE_EXT) else {
476 continue;
477 };
478
479 if !selected_scoped.iter().any(|n| n == addon_name_wo_suffix) {
480 continue;
481 }
482
483 let final_cmdline_has_cfs = cmdline_has_composefs(final_cmdline);
484 let addon_cmdline = get_cmdline_in_addon(&addon_dir, &addon_name)?;
485
486 if let Some(addon_cmdline) = addon_cmdline {
487 let cfs_in_addon_cmdline = cmdline_has_composefs(&addon_cmdline);
488
489 if final_cmdline_has_cfs && cfs_in_addon_cmdline {
490 anyhow::bail!(
491 "Multiple composefs cmdlines found across the UKI and its addons (duplicate in scoped addon {addon_name})"
492 );
493 }
494
495 final_cmdline.extend(&addon_cmdline);
496 };
497 }
498 }
499
500 Ok(())
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506 use bootc_utils::create_minimal_pe;
507 use cap_std_ext::{cap_std, cap_tempfile, dirext::CapStdExtDirExt};
508
509 fn android_image() -> Vec<u8> {
510 let mut image = vec![0; 4097];
511 image[..8].copy_from_slice(b"ANDROID!");
512 for (offset, value) in [(8, 1u32), (16, 1), (36, 2048), (40, 2), (1644, 1660)] {
513 image[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
514 }
515 image[64..69].copy_from_slice(b"quiet");
516 image[2048] = 1;
517 image[4096] = 2;
518 image
519 }
520
521 fn aboot_uki() -> Vec<u8> {
522 let mut image = create_minimal_pe();
523 image[0x86..0x88].copy_from_slice(&3u16.to_le_bytes());
524 let headers = 0x188;
525 for (i, name) in [b".linux\0\0", b".initrd\0"].iter().enumerate() {
526 let offset = headers + 40 * (i + 1);
527 image[offset..offset + 8].copy_from_slice(*name);
528 image[offset + 8..offset + 12].copy_from_slice(&1u32.to_le_bytes());
529 image[offset + 16..offset + 20].copy_from_slice(&1u32.to_le_bytes());
530 image[offset + 20..offset + 24].copy_from_slice(&0x200u32.to_le_bytes());
531 }
532 image
533 }
534
535 #[test]
536 fn test_aboot_kernel_detection() -> Result<()> {
537 for (payload, encoding, image_type) in [
538 (
539 android_image(),
540 AbootEncoding::AndroidV2,
541 ContainerImageType::Aboot,
542 ),
543 (
544 aboot_uki(),
545 AbootEncoding::Uki,
546 ContainerImageType::AbootEfi,
547 ),
548 ] {
549 let root = cap_tempfile::tempdir(cap_std::ambient_authority())?;
550 root.create_dir("boot")?;
551 root.write("boot/aboot-6.12.img", payload)?;
552 let kernel = find_kernel(&root, None)?.unwrap();
553 assert_eq!(kernel.kernel.version, "6.12");
554 assert_eq!(kernel.k_type.image_type(), image_type);
555 assert!(kernel.kernel.unified);
556 assert!(kernel.k_type.is_unified());
557 assert!(
558 kernel
559 .k_type
560 .cmdline()
561 .unwrap()
562 .to_string()
563 .contains("quiet")
564 );
565 let KernelType::Aboot {
566 path,
567 encoding: actual,
568 ..
569 } = kernel.k_type
570 else {
571 panic!()
572 };
573 assert_eq!(path, "boot/aboot-6.12.img");
574 assert_eq!(actual, encoding);
575 root.write("boot/aboot-6.13.img", android_image())?;
576 assert!(find_kernel(&root, None).is_err());
577 }
578 Ok(())
579 }
580
581 #[test]
582 fn test_aboot_detection_validation() -> Result<()> {
583 for (files, valid) in [
584 (vec![("boot/aboot-6.12.img", android_image())], true),
585 (
586 vec![
587 ("boot/aboot-6.12.img", android_image()),
588 ("boot/EFI/Linux/slot.addon.efi", create_minimal_pe()),
589 ],
590 true,
591 ),
592 (
593 vec![
594 ("boot/aboot-6.12.img", android_image()),
595 ("boot/loader/entries/example.conf", vec![0]),
596 ],
597 false,
598 ),
599 (vec![("boot/aboot-6.12.img", b"ANDROID!".to_vec())], false),
600 (vec![("boot/aboot-.img", android_image())], false),
601 (vec![("boot/vbmeta-6.12.img", vec![0])], false),
602 (
603 vec![
604 ("boot/aboot-6.12.img", aboot_uki()),
605 ("boot/vbmeta-6.12.img", vec![0]),
606 ],
607 false,
608 ),
609 (
610 vec![
611 ("boot/aboot-6.12.img", android_image()),
612 ("usr/lib/modules/6.12/vmlinuz", vec![0]),
613 ],
614 false,
615 ),
616 ] {
617 let root = cap_tempfile::tempdir(cap_std::ambient_authority())?;
618 for (path, contents) in files {
619 let path = std::path::Path::new(path);
620 root.create_dir_all(path.parent().unwrap())?;
621 root.write(path, contents)?;
622 }
623 assert_eq!(find_kernel(&root, None).is_ok(), valid);
624 }
625 let root = cap_tempfile::tempdir(cap_std::ambient_authority())?;
626 root.create_dir_all("usr/lib/modules/6.12")?;
627 root.write("usr/lib/modules/6.12/vmlinuz", b"kernel")?;
628 root.write("usr/lib/modules/6.12/aboot.img", b"legacy")?;
629 assert!(matches!(
630 find_kernel(&root, None)?.unwrap().k_type,
631 KernelType::Vmlinuz { .. }
632 ));
633 Ok(())
634 }
635
636 #[test]
637 fn test_find_kernel_none() -> Result<()> {
638 let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
639 assert!(find_kernel(&tempdir, None)?.is_none());
640 Ok(())
641 }
642
643 #[test]
644 fn test_find_kernel_traditional() -> Result<()> {
645 let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
646 tempdir.create_dir_all("usr/lib/modules/6.12.0-100.fc41.x86_64")?;
647 tempdir.atomic_write(
648 "usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz",
649 b"fake kernel",
650 )?;
651
652 let kernel_internal = find_kernel(&tempdir, None)?.expect("should find kernel");
653 assert_eq!(kernel_internal.kernel.version, "6.12.0-100.fc41.x86_64");
654 assert!(!kernel_internal.kernel.unified);
655 assert!(!kernel_internal.k_type.is_unified());
656 assert!(kernel_internal.k_type.cmdline().is_none());
657 match &kernel_internal.k_type {
658 KernelType::Vmlinuz { path, initramfs } => {
659 assert_eq!(
660 path.as_str(),
661 "usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz"
662 );
663 assert_eq!(
664 initramfs.as_str(),
665 "usr/lib/modules/6.12.0-100.fc41.x86_64/initramfs.img"
666 );
667 }
668 _ => panic!("Expected Vmlinuz"),
669 }
670 Ok(())
671 }
672
673 #[test]
674 fn test_find_kernel_uki() -> Result<()> {
675 let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
676 tempdir.create_dir_all("boot/EFI/Linux")?;
677 tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", &create_minimal_pe())?;
678
679 let kernel_internal = find_kernel(&tempdir, None)?.expect("should find kernel");
680 assert_eq!(kernel_internal.kernel.version, "fedora-6.12.0");
681 assert!(kernel_internal.kernel.unified);
682 assert!(kernel_internal.k_type.is_unified());
683 match &kernel_internal.k_type {
684 KernelType::Uki { path, .. } => {
685 assert_eq!(path.as_str(), "boot/EFI/Linux/fedora-6.12.0.efi");
686 }
687 _ => panic!("Expected Uki"),
688 }
689 Ok(())
690 }
691
692 #[test]
693 fn test_find_kernel_uki_takes_precedence() -> Result<()> {
694 let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
695 tempdir.create_dir_all("usr/lib/modules/6.12.0-100.fc41.x86_64")?;
697 tempdir.atomic_write(
698 "usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz",
699 b"fake kernel",
700 )?;
701 tempdir.create_dir_all("boot/EFI/Linux")?;
702 tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", &create_minimal_pe())?;
703
704 let kernel_internal = find_kernel(&tempdir, None)?.expect("should find kernel");
705 assert_eq!(kernel_internal.kernel.version, "fedora-6.12.0");
707 assert!(kernel_internal.kernel.unified);
708 Ok(())
709 }
710
711 #[test]
712 fn test_split_kernel() -> Result<()> {
713 const KVER: &str = "6.12.0-100.fc41.x86_64";
714 let moddir = format!("usr/lib/modules/{KVER}");
715 const STAYS: &[&str] = &["modules.dep", "config", "System.map"];
717 let cases = std::iter::once(&[][..])
719 .chain(KERNEL_COMPANION_FILES.chunks(1))
720 .chain(std::iter::once(KERNEL_COMPANION_FILES));
721 for present in cases {
722 let root = cap_tempfile::tempdir(cap_std::ambient_authority())?;
723 let output = cap_tempfile::tempdir(cap_std::ambient_authority())?;
724 root.create_dir_all(&moddir)?;
725 root.atomic_write(format!("{moddir}/vmlinuz"), b"kernel")?;
726 root.atomic_write(format!("{moddir}/initramfs.img"), b"initramfs")?;
727 for name in STAYS.iter().chain(present) {
728 root.atomic_write(format!("{moddir}/{name}"), name)?;
729 }
730
731 assert_eq!(split_kernel(&root, &output)?, KVER);
732
733 let mut remaining: Vec<_> = root
734 .read_dir(&moddir)?
735 .map(|e| e.map(|e| e.file_name()))
736 .collect::<std::io::Result<_>>()?;
737 remaining.sort();
738 let mut expected = STAYS.to_vec();
739 expected.sort();
740 assert_eq!(remaining, expected, "{present:?}");
741
742 let dest = output.open_dir(KVER)?;
743 assert_eq!(dest.read("vmlinuz")?, b"kernel");
744 assert_eq!(dest.read("initramfs.img")?, b"initramfs");
745 for &name in KERNEL_COMPANION_FILES {
746 if present.contains(&name) {
747 assert_eq!(dest.read(name)?, name.as_bytes(), "{name}");
748 } else {
749 assert!(!dest.try_exists(name)?, "{name}");
750 }
751 }
752 }
753 Ok(())
754 }
755
756 #[test]
757 fn test_split_kernel_uki() -> Result<()> {
758 let root = cap_tempfile::tempdir(cap_std::ambient_authority())?;
759 let output = cap_tempfile::tempdir(cap_std::ambient_authority())?;
760 root.create_dir_all("boot/EFI/Linux")?;
761 root.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", &create_minimal_pe())?;
762 assert!(split_kernel(&root, &output).is_err());
763 assert!(split_kernel(&output, &root).is_err());
765 Ok(())
766 }
767
768 #[test]
769 fn test_find_uki_path_sorted() -> Result<()> {
770 let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
771 tempdir.create_dir_all("boot/EFI/Linux")?;
772 tempdir.atomic_write("boot/EFI/Linux/zzz.efi", &create_minimal_pe())?;
773 tempdir.atomic_write("boot/EFI/Linux/aaa.efi", &create_minimal_pe())?;
774 tempdir.atomic_write("boot/EFI/Linux/mmm.efi", &create_minimal_pe())?;
775
776 let path = find_uki_path(&tempdir)?.expect("should find uki");
778 assert_eq!(path.as_str(), "boot/EFI/Linux/aaa.efi");
779 Ok(())
780 }
781
782 #[test]
783 fn test_find_kernel_with_addons() -> Result<()> {
784 for (has_uki, has_vmlinuz, has_cmdline, expected_unified) in [
785 (true, false, true, Some(true)),
786 (false, false, false, None),
787 (false, true, false, Some(false)),
788 (true, false, false, Some(true)),
789 ] {
790 let root = cap_tempfile::tempdir(cap_std::ambient_authority())?;
791 root.create_dir_all("boot/EFI/Linux")?;
792 root.write("boot/EFI/Linux/000.addon.efi", create_minimal_pe())?;
793 if has_uki {
794 let mut uki = create_minimal_pe();
795 if !has_cmdline {
796 const SECTION_HEADER_OFFSET: usize = 0x188;
797 uki[SECTION_HEADER_OFFSET..SECTION_HEADER_OFFSET + 8]
798 .copy_from_slice(b".linux\0\0");
799 }
800 root.write("boot/EFI/Linux/kernel.efi", uki)?;
801 }
802 if has_vmlinuz {
803 root.create_dir_all("usr/lib/modules/6.12")?;
804 root.write("usr/lib/modules/6.12/vmlinuz", b"kernel")?;
805 }
806
807 let kernel = find_kernel(&root, None)?;
808 assert_eq!(kernel.as_ref().map(|k| k.kernel.unified), expected_unified);
809 if let Some(kernel) = kernel {
810 assert_eq!(
811 kernel.kernel.version,
812 if has_uki { "kernel" } else { "6.12" }
813 );
814 if let KernelType::Uki { path, cmdline } = kernel.k_type {
815 assert_eq!(path, "boot/EFI/Linux/kernel.efi");
816 assert_eq!(
817 cmdline.as_ref().map(ToString::to_string).as_deref(),
818 has_cmdline.then_some("quiet splash").or_else(|| Some(""))
819 );
820 } else {
821 let KernelType::Vmlinuz { path, .. } = kernel.k_type else {
822 panic!("Expected vmlinuz");
823 };
824 assert_eq!(path, "usr/lib/modules/6.12/vmlinuz");
825 }
826 }
827 }
828 Ok(())
829 }
830}