1use std::{collections::HashSet, io::Read, sync::OnceLock};
2
3use anyhow::{Context, Result};
4use bootc_kernel_cmdline::utf8::Cmdline;
5use bootc_mount::inspect_filesystem;
6use fn_error_context::context;
7use serde::{Deserialize, Serialize};
8
9use crate::{
10 bootc_composefs::{
11 boot::BootType,
12 repo::get_imgref,
13 utils::{compute_store_boot_digest_for_uki, get_uki_cmdline},
14 },
15 composefs_consts::{
16 COMPOSEFS_CMDLINE, ORIGIN_KEY_BOOT_DIGEST, TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED, USER_CFG,
17 },
18 install::EFI_LOADER_INFO,
19 parsers::{
20 bls_config::{parse_bls_config, BLSConfig, BLSConfigType},
21 grub_menuconfig::{parse_grub_menuentry_file, MenuEntry},
22 },
23 spec::{BootEntry, BootOrder, Host, HostSpec, ImageReference, ImageStatus},
24 store::Storage,
25 utils::{read_uefi_var, EfiError},
26};
27
28use std::str::FromStr;
29
30use bootc_utils::try_deserialize_timestamp;
31use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt};
32use ostree_container::OstreeImageReference;
33use ostree_ext::container::{self as ostree_container};
34use ostree_ext::containers_image_proxy;
35use ostree_ext::oci_spec;
36use ostree_ext::{container::deploy::ORIGIN_CONTAINER, oci_spec::image::ImageConfiguration};
37
38use ostree_ext::oci_spec::image::ImageManifest;
39use tokio::io::AsyncReadExt;
40
41use crate::composefs_consts::{
42 COMPOSEFS_STAGED_DEPLOYMENT_FNAME, COMPOSEFS_TRANSIENT_STATE_DIR, ORIGIN_KEY_BOOT,
43 ORIGIN_KEY_BOOT_TYPE, STATE_DIR_RELATIVE,
44};
45use crate::spec::Bootloader;
46
47#[derive(Debug, Serialize, Deserialize)]
49pub(crate) struct ImgConfigManifest {
50 pub(crate) config: ImageConfiguration,
51 pub(crate) manifest: ImageManifest,
52}
53
54#[derive(Clone)]
56pub(crate) struct ComposefsCmdline {
57 #[allow(dead_code)]
58 pub insecure: bool,
59 pub digest: Box<str>,
60}
61
62impl ComposefsCmdline {
63 pub(crate) fn new(s: &str) -> Self {
64 let (insecure, digest_str) = s
65 .strip_prefix('?')
66 .map(|v| (true, v))
67 .unwrap_or_else(|| (false, s));
68 ComposefsCmdline {
69 insecure,
70 digest: digest_str.into(),
71 }
72 }
73}
74
75impl std::fmt::Display for ComposefsCmdline {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 let insecure = if self.insecure { "?" } else { "" };
78 write!(f, "{}={}{}", COMPOSEFS_CMDLINE, insecure, self.digest)
79 }
80}
81
82pub(crate) fn composefs_booted() -> Result<Option<&'static ComposefsCmdline>> {
84 static CACHED_DIGEST_VALUE: OnceLock<Option<ComposefsCmdline>> = OnceLock::new();
85 if let Some(v) = CACHED_DIGEST_VALUE.get() {
86 return Ok(v.as_ref());
87 }
88 let cmdline = Cmdline::from_proc()?;
89 let Some(kv) = cmdline.find(COMPOSEFS_CMDLINE) else {
90 return Ok(None);
91 };
92 let Some(v) = kv.value() else { return Ok(None) };
93 let v = ComposefsCmdline::new(v);
94
95 let root_mnt = inspect_filesystem("/".into())?;
97
98 let verity_from_mount_src = root_mnt
100 .source
101 .strip_prefix("composefs:")
102 .ok_or_else(|| anyhow::anyhow!("Root not mounted using composefs"))?;
103
104 let r = if *verity_from_mount_src != *v.digest {
105 CACHED_DIGEST_VALUE.get_or_init(|| Some(ComposefsCmdline::new(verity_from_mount_src)))
107 } else {
108 CACHED_DIGEST_VALUE.get_or_init(|| Some(v))
109 };
110
111 Ok(r.as_ref())
112}
113
114pub(crate) fn get_sorted_grub_uki_boot_entries<'a>(
116 boot_dir: &Dir,
117 str: &'a mut String,
118) -> Result<Vec<MenuEntry<'a>>> {
119 let mut file = boot_dir
120 .open(format!("grub2/{USER_CFG}"))
121 .with_context(|| format!("Opening {USER_CFG}"))?;
122 file.read_to_string(str)?;
123 parse_grub_menuentry_file(str)
124}
125
126pub(crate) fn get_sorted_type1_boot_entries(
127 boot_dir: &Dir,
128 ascending: bool,
129) -> Result<Vec<BLSConfig>> {
130 get_sorted_type1_boot_entries_helper(boot_dir, ascending, false)
131}
132
133pub(crate) fn get_sorted_staged_type1_boot_entries(
134 boot_dir: &Dir,
135 ascending: bool,
136) -> Result<Vec<BLSConfig>> {
137 get_sorted_type1_boot_entries_helper(boot_dir, ascending, true)
138}
139
140#[context("Getting sorted Type1 boot entries")]
141fn get_sorted_type1_boot_entries_helper(
142 boot_dir: &Dir,
143 ascending: bool,
144 get_staged_entries: bool,
145) -> Result<Vec<BLSConfig>> {
146 let mut all_configs = vec![];
147
148 let dir = match get_staged_entries {
149 true => {
150 let dir = boot_dir.open_dir_optional(TYPE1_ENT_PATH_STAGED)?;
151
152 let Some(dir) = dir else {
153 return Ok(all_configs);
154 };
155
156 dir.read_dir(".")?
157 }
158
159 false => boot_dir.read_dir(TYPE1_ENT_PATH)?,
160 };
161
162 for entry in dir {
163 let entry = entry?;
164
165 let file_name = entry.file_name();
166
167 let file_name = file_name
168 .to_str()
169 .ok_or(anyhow::anyhow!("Found non UTF-8 characters in filename"))?;
170
171 if !file_name.ends_with(".conf") {
172 continue;
173 }
174
175 let mut file = entry
176 .open()
177 .with_context(|| format!("Failed to open {:?}", file_name))?;
178
179 let mut contents = String::new();
180 file.read_to_string(&mut contents)
181 .with_context(|| format!("Failed to read {:?}", file_name))?;
182
183 let config = parse_bls_config(&contents).context("Parsing bls config")?;
184
185 all_configs.push(config);
186 }
187
188 all_configs.sort_by(|a, b| if ascending { a.cmp(b) } else { b.cmp(a) });
189
190 Ok(all_configs)
191}
192
193#[context("Getting container info")]
195pub(crate) async fn get_container_manifest_and_config(
196 imgref: &String,
197) -> Result<ImgConfigManifest> {
198 let config = containers_image_proxy::ImageProxyConfig::default();
199 let proxy = containers_image_proxy::ImageProxy::new_with_config(config).await?;
200
201 let img = proxy
202 .open_image(&imgref)
203 .await
204 .with_context(|| format!("Opening image {imgref}"))?;
205
206 let (_, manifest) = proxy.fetch_manifest(&img).await?;
207 let (mut reader, driver) = proxy.get_descriptor(&img, manifest.config()).await?;
208
209 let mut buf = Vec::with_capacity(manifest.config().size() as usize);
210 buf.resize(manifest.config().size() as usize, 0);
211 reader.read_exact(&mut buf).await?;
212 driver.await?;
213
214 let config: oci_spec::image::ImageConfiguration = serde_json::from_slice(&buf)?;
215
216 Ok(ImgConfigManifest { manifest, config })
217}
218
219#[context("Getting bootloader")]
220pub(crate) fn get_bootloader() -> Result<Bootloader> {
221 match read_uefi_var(EFI_LOADER_INFO) {
222 Ok(loader) => {
223 if loader.to_lowercase().contains("systemd-boot") {
224 return Ok(Bootloader::Systemd);
225 }
226
227 return Ok(Bootloader::Grub);
228 }
229
230 Err(efi_error) => match efi_error {
231 EfiError::SystemNotUEFI => return Ok(Bootloader::Grub),
232 EfiError::MissingVar => return Ok(Bootloader::Grub),
233
234 e => return Err(anyhow::anyhow!("Failed to read EfiLoaderInfo: {e:?}")),
235 },
236 }
237}
238
239#[context("Reading imginfo")]
241pub(crate) async fn get_imginfo(
242 storage: &Storage,
243 deployment_id: &str,
244 imgref: Option<&ImageReference>,
245) -> Result<ImgConfigManifest> {
246 let imginfo_fname = format!("{deployment_id}.imginfo");
247
248 let depl_state_path = std::path::PathBuf::from(STATE_DIR_RELATIVE).join(deployment_id);
249 let path = depl_state_path.join(imginfo_fname);
250
251 let mut img_conf = storage
252 .physical_root
253 .open_optional(&path)
254 .context("Failed to open file")?;
255
256 let Some(img_conf) = &mut img_conf else {
257 let imgref = imgref.ok_or_else(|| anyhow::anyhow!("No imgref or imginfo file found"))?;
258
259 let container_details =
260 get_container_manifest_and_config(&get_imgref(&imgref.transport, &imgref.image))
261 .await?;
262
263 let state_dir = storage.physical_root.open_dir(depl_state_path)?;
264
265 state_dir
266 .atomic_write(
267 format!("{}.imginfo", deployment_id),
268 serde_json::to_vec(&container_details)?,
269 )
270 .context("Failed to write to .imginfo file")?;
271
272 let state_dir = state_dir.reopen_as_ownedfd()?;
273
274 rustix::fs::fsync(state_dir).context("fsync")?;
275
276 return Ok(container_details);
277 };
278
279 let mut buffer = String::new();
280 img_conf.read_to_string(&mut buffer)?;
281
282 let img_conf = serde_json::from_str::<ImgConfigManifest>(&buffer)
283 .context("Failed to parse file as JSON")?;
284
285 Ok(img_conf)
286}
287
288#[context("Getting composefs deployment metadata")]
289async fn boot_entry_from_composefs_deployment(
290 storage: &Storage,
291 origin: tini::Ini,
292 verity: String,
293) -> Result<BootEntry> {
294 let image = match origin.get::<String>("origin", ORIGIN_CONTAINER) {
295 Some(img_name_from_config) => {
296 let ostree_img_ref = OstreeImageReference::from_str(&img_name_from_config)?;
297 let img_ref = ImageReference::from(ostree_img_ref);
298
299 let img_conf = get_imginfo(storage, &verity, Some(&img_ref)).await?;
300
301 let image_digest = img_conf.manifest.config().digest().to_string();
302 let architecture = img_conf.config.architecture().to_string();
303 let version = img_conf
304 .manifest
305 .annotations()
306 .as_ref()
307 .and_then(|a| a.get(oci_spec::image::ANNOTATION_VERSION).cloned());
308
309 let created_at = img_conf.config.created().clone();
310 let timestamp = created_at.and_then(|x| try_deserialize_timestamp(&x));
311
312 Some(ImageStatus {
313 image: img_ref,
314 version,
315 timestamp,
316 image_digest,
317 architecture,
318 })
319 }
320
321 None => None,
323 };
324
325 let boot_type = match origin.get::<String>(ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_TYPE) {
326 Some(s) => BootType::try_from(s.as_str())?,
327 None => anyhow::bail!("{ORIGIN_KEY_BOOT} not found"),
328 };
329
330 let boot_digest = origin.get::<String>(ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST);
331
332 let e = BootEntry {
333 image,
334 cached_update: None,
335 incompatible: false,
336 pinned: false,
337 download_only: false, store: None,
339 ostree: None,
340 composefs: Some(crate::spec::BootEntryComposefs {
341 verity,
342 boot_type,
343 bootloader: get_bootloader()?,
344 boot_digest,
345 }),
346 soft_reboot_capable: false,
347 };
348
349 Ok(e)
350}
351
352#[context("Getting composefs deployment status")]
355pub(crate) async fn get_composefs_status(
356 storage: &crate::store::Storage,
357 booted_cfs: &crate::store::BootedComposefs,
358) -> Result<Host> {
359 composefs_deployment_status_from(&storage, booted_cfs.cmdline).await
360}
361
362#[context("Checking soft reboot capability")]
364fn set_soft_reboot_capability(
365 storage: &Storage,
366 host: &mut Host,
367 bls_entries: Option<Vec<BLSConfig>>,
368 cmdline: &ComposefsCmdline,
369) -> Result<()> {
370 let booted = host.require_composefs_booted()?;
371
372 match booted.boot_type {
373 BootType::Bls => {
374 let mut bls_entries =
375 bls_entries.ok_or_else(|| anyhow::anyhow!("BLS entries not provided"))?;
376
377 let staged_entries =
378 get_sorted_staged_type1_boot_entries(storage.require_boot_dir()?, false)?;
379
380 bls_entries.extend(staged_entries);
383
384 set_reboot_capable_type1_deployments(cmdline, host, bls_entries)
385 }
386
387 BootType::Uki => set_reboot_capable_uki_deployments(storage, cmdline, host),
388 }
389}
390
391fn find_bls_entry<'a>(
392 verity: &str,
393 bls_entries: &'a Vec<BLSConfig>,
394) -> Result<Option<&'a BLSConfig>> {
395 for ent in bls_entries {
396 if ent.get_verity()? == *verity {
397 return Ok(Some(ent));
398 }
399 }
400
401 Ok(None)
402}
403
404fn compare_cmdline_skip_cfs(first: &Cmdline<'_>, second: &Cmdline<'_>) -> bool {
406 for param in first {
407 if param.key() == COMPOSEFS_CMDLINE.into() {
408 continue;
409 }
410
411 let second_param = second.iter().find(|b| *b == param);
412
413 let Some(found_param) = second_param else {
414 return false;
415 };
416
417 if found_param.value() != param.value() {
418 return false;
419 }
420 }
421
422 return true;
423}
424
425#[context("Setting soft reboot capability for Type1 entries")]
426fn set_reboot_capable_type1_deployments(
427 booted_cmdline: &ComposefsCmdline,
428 host: &mut Host,
429 bls_entries: Vec<BLSConfig>,
430) -> Result<()> {
431 let booted = host
432 .status
433 .booted
434 .as_ref()
435 .ok_or_else(|| anyhow::anyhow!("Failed to find booted entry"))?;
436
437 let booted_boot_digest = booted.composefs_boot_digest()?;
438
439 let booted_bls_entry = find_bls_entry(&*booted_cmdline.digest, &bls_entries)?
440 .ok_or_else(|| anyhow::anyhow!("Booted BLS entry not found"))?;
441
442 let booted_cmdline = booted_bls_entry.get_cmdline()?;
443
444 for depl in host
445 .status
446 .staged
447 .iter_mut()
448 .chain(host.status.rollback.iter_mut())
449 .chain(host.status.other_deployments.iter_mut())
450 {
451 let entry = find_bls_entry(&depl.require_composefs()?.verity, &bls_entries)?
452 .ok_or_else(|| anyhow::anyhow!("Entry not found"))?;
453
454 let depl_cmdline = entry.get_cmdline()?;
455
456 depl.soft_reboot_capable = is_soft_rebootable(
457 depl.composefs_boot_digest()?,
458 booted_boot_digest,
459 depl_cmdline,
460 booted_cmdline,
461 );
462 }
463
464 Ok(())
465}
466
467fn is_soft_rebootable(
468 depl_boot_digest: &str,
469 booted_boot_digest: &str,
470 depl_cmdline: &Cmdline,
471 booted_cmdline: &Cmdline,
472) -> bool {
473 if depl_boot_digest != booted_boot_digest {
474 tracing::debug!("Soft reboot not allowed due to kernel skew");
475 return false;
476 }
477
478 if depl_cmdline.as_bytes().len() != booted_cmdline.as_bytes().len() {
479 tracing::debug!("Soft reboot not allowed due to differing cmdline");
480 return false;
481 }
482
483 return compare_cmdline_skip_cfs(depl_cmdline, booted_cmdline)
484 && compare_cmdline_skip_cfs(booted_cmdline, depl_cmdline);
485}
486
487#[context("Setting soft reboot capability for UKI deployments")]
488fn set_reboot_capable_uki_deployments(
489 storage: &Storage,
490 cmdline: &ComposefsCmdline,
491 host: &mut Host,
492) -> Result<()> {
493 let booted = host
494 .status
495 .booted
496 .as_ref()
497 .ok_or_else(|| anyhow::anyhow!("Failed to find booted entry"))?;
498
499 let booted_boot_digest = match booted.composefs_boot_digest() {
501 Ok(d) => d,
502 Err(_) => &compute_store_boot_digest_for_uki(storage, &cmdline.digest)?,
503 };
504
505 let booted_cmdline = get_uki_cmdline(storage, &booted.require_composefs()?.verity)?;
506
507 for deployment in host
508 .status
509 .staged
510 .iter_mut()
511 .chain(host.status.rollback.iter_mut())
512 .chain(host.status.other_deployments.iter_mut())
513 {
514 let depl_boot_digest = match deployment.composefs_boot_digest() {
516 Ok(d) => d,
517 Err(_) => &compute_store_boot_digest_for_uki(
518 storage,
519 &deployment.require_composefs()?.verity,
520 )?,
521 };
522
523 let depl_cmdline = get_uki_cmdline(storage, &deployment.require_composefs()?.verity)?;
524
525 deployment.soft_reboot_capable = is_soft_rebootable(
526 depl_boot_digest,
527 booted_boot_digest,
528 &depl_cmdline,
529 &booted_cmdline,
530 );
531 }
532
533 Ok(())
534}
535
536#[context("Getting composefs deployment status")]
537pub(crate) async fn composefs_deployment_status_from(
538 storage: &Storage,
539 cmdline: &ComposefsCmdline,
540) -> Result<Host> {
541 let booted_composefs_digest = &cmdline.digest;
542
543 let boot_dir = storage.require_boot_dir()?;
544
545 let deployments = storage
546 .physical_root
547 .read_dir(STATE_DIR_RELATIVE)
548 .with_context(|| format!("Reading sysroot {STATE_DIR_RELATIVE}"))?;
549
550 let host_spec = HostSpec {
551 image: None,
552 boot_order: BootOrder::Default,
553 };
554
555 let mut host = Host::new(host_spec);
556
557 let staged_deployment_id = match std::fs::File::open(format!(
558 "{COMPOSEFS_TRANSIENT_STATE_DIR}/{COMPOSEFS_STAGED_DEPLOYMENT_FNAME}"
559 )) {
560 Ok(mut f) => {
561 let mut s = String::new();
562 f.read_to_string(&mut s)?;
563
564 Ok(Some(s))
565 }
566 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
567 Err(e) => Err(e),
568 }?;
569
570 let mut boot_type: Option<BootType> = None;
572
573 let mut extra_deployment_boot_entries: Vec<BootEntry> = Vec::new();
576
577 for depl in deployments {
578 let depl = depl?;
579
580 let depl_file_name = depl.file_name();
581 let depl_file_name = depl_file_name.to_string_lossy();
582
583 let config = depl
585 .open_dir()
586 .with_context(|| format!("Failed to open {depl_file_name}"))?
587 .read_to_string(format!("{depl_file_name}.origin"))
588 .with_context(|| format!("Reading file {depl_file_name}.origin"))?;
589
590 let ini = tini::Ini::from_string(&config)
591 .with_context(|| format!("Failed to parse file {depl_file_name}.origin as ini"))?;
592
593 let boot_entry =
594 boot_entry_from_composefs_deployment(storage, ini, depl_file_name.to_string()).await?;
595
596 let boot_type_from_origin = boot_entry.composefs.as_ref().unwrap().boot_type;
598
599 match boot_type {
600 Some(current_type) => {
601 if current_type != boot_type_from_origin {
602 anyhow::bail!("Conflicting boot types")
603 }
604 }
605
606 None => {
607 boot_type = Some(boot_type_from_origin);
608 }
609 };
610
611 if depl.file_name() == booted_composefs_digest.as_ref() {
612 host.spec.image = boot_entry.image.as_ref().map(|x| x.image.clone());
613 host.status.booted = Some(boot_entry);
614 continue;
615 }
616
617 if let Some(staged_deployment_id) = &staged_deployment_id {
618 if depl_file_name == staged_deployment_id.trim() {
619 host.status.staged = Some(boot_entry);
620 continue;
621 }
622 }
623
624 extra_deployment_boot_entries.push(boot_entry);
625 }
626
627 let Some(boot_type) = boot_type else {
629 anyhow::bail!("Could not determine boot type");
630 };
631
632 let booted_cfs = host.require_composefs_booted()?;
633
634 let mut grub_menu_string = String::new();
635 let (is_rollback_queued, sorted_bls_config, grub_menu_entries) = match booted_cfs.bootloader {
636 Bootloader::Grub => match boot_type {
637 BootType::Bls => {
638 let bls_configs = get_sorted_type1_boot_entries(boot_dir, false)?;
639 let bls_config = bls_configs
640 .first()
641 .ok_or_else(|| anyhow::anyhow!("First boot entry not found"))?;
642
643 match &bls_config.cfg_type {
644 BLSConfigType::NonEFI { options, .. } => {
645 let is_rollback_queued = !options
646 .as_ref()
647 .ok_or_else(|| anyhow::anyhow!("options key not found in bls config"))?
648 .contains(booted_composefs_digest.as_ref());
649
650 (is_rollback_queued, Some(bls_configs), None)
651 }
652
653 BLSConfigType::EFI { .. } => {
654 anyhow::bail!("Found 'efi' field in Type1 boot entry")
655 }
656
657 BLSConfigType::Unknown => anyhow::bail!("Unknown BLS Config Type"),
658 }
659 }
660
661 BootType::Uki => {
662 let menuentries =
663 get_sorted_grub_uki_boot_entries(boot_dir, &mut grub_menu_string)?;
664
665 let is_rollback_queued = !menuentries
666 .first()
667 .ok_or(anyhow::anyhow!("First boot entry not found"))?
668 .body
669 .chainloader
670 .contains(booted_composefs_digest.as_ref());
671
672 (is_rollback_queued, None, Some(menuentries))
673 }
674 },
675
676 Bootloader::Systemd => {
678 let bls_configs = get_sorted_type1_boot_entries(boot_dir, true)?;
679 let bls_config = bls_configs
680 .first()
681 .ok_or(anyhow::anyhow!("First boot entry not found"))?;
682
683 let is_rollback_queued = match &bls_config.cfg_type {
684 BLSConfigType::EFI { efi } => {
686 efi.as_str().contains(booted_composefs_digest.as_ref())
687 }
688
689 BLSConfigType::NonEFI { options, .. } => !options
691 .as_ref()
692 .ok_or(anyhow::anyhow!("options key not found in bls config"))?
693 .contains(booted_composefs_digest.as_ref()),
694
695 BLSConfigType::Unknown => anyhow::bail!("Unknown BLS Config Type"),
696 };
697
698 (is_rollback_queued, Some(bls_configs), None)
699 }
700 };
701
702 let bootloader_configured_verity = sorted_bls_config
705 .iter()
706 .flatten()
707 .map(|cfg| cfg.get_verity())
708 .chain(
709 grub_menu_entries
710 .iter()
711 .flatten()
712 .map(|menu| menu.get_verity()),
713 )
714 .collect::<Result<HashSet<_>>>()?;
715 let rollback_candidates: Vec<_> = extra_deployment_boot_entries
716 .into_iter()
717 .filter(|entry| {
718 let verity = &entry
719 .composefs
720 .as_ref()
721 .expect("composefs is always Some for composefs deployments")
722 .verity;
723 bootloader_configured_verity.contains(verity)
724 })
725 .collect();
726
727 if rollback_candidates.len() > 1 {
728 anyhow::bail!("Multiple extra entries in /boot, could not determine rollback entry");
729 } else if let Some(rollback_entry) = rollback_candidates.into_iter().next() {
730 host.status.rollback = Some(rollback_entry);
731 }
732
733 host.status.rollback_queued = is_rollback_queued;
734
735 if host.status.rollback_queued {
736 host.spec.boot_order = BootOrder::Rollback
737 };
738
739 set_soft_reboot_capability(storage, &mut host, sorted_bls_config, cmdline)?;
740
741 Ok(host)
742}
743
744#[cfg(test)]
745mod tests {
746 use cap_std_ext::{cap_std, dirext::CapStdExtDirExt};
747
748 use crate::parsers::{bls_config::BLSConfigType, grub_menuconfig::MenuentryBody};
749
750 use super::*;
751
752 #[test]
753 fn test_composefs_parsing() {
754 const DIGEST: &str = "8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52";
755 let v = ComposefsCmdline::new(DIGEST);
756 assert!(!v.insecure);
757 assert_eq!(v.digest.as_ref(), DIGEST);
758 let v = ComposefsCmdline::new(&format!("?{}", DIGEST));
759 assert!(v.insecure);
760 assert_eq!(v.digest.as_ref(), DIGEST);
761 }
762
763 #[test]
764 fn test_sorted_bls_boot_entries() -> Result<()> {
765 let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
766
767 let entry1 = r#"
768 title Fedora 42.20250623.3.1 (CoreOS)
769 version fedora-42.0
770 sort-key 1
771 linux /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10
772 initrd /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img
773 options root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6
774 "#;
775
776 let entry2 = r#"
777 title Fedora 41.20250214.2.0 (CoreOS)
778 version fedora-42.0
779 sort-key 2
780 linux /boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/vmlinuz-5.14.10
781 initrd /boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/initramfs-5.14.10.img
782 options root=UUID=abc123 rw composefs=febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01
783 "#;
784
785 tempdir.create_dir_all("loader/entries")?;
786 tempdir.atomic_write(
787 "loader/entries/random_file.txt",
788 "Random file that we won't parse",
789 )?;
790 tempdir.atomic_write("loader/entries/entry1.conf", entry1)?;
791 tempdir.atomic_write("loader/entries/entry2.conf", entry2)?;
792
793 let result = get_sorted_type1_boot_entries(&tempdir, true).unwrap();
794
795 let mut config1 = BLSConfig::default();
796 config1.title = Some("Fedora 42.20250623.3.1 (CoreOS)".into());
797 config1.sort_key = Some("1".into());
798 config1.cfg_type = BLSConfigType::NonEFI {
799 linux: "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10".into(),
800 initrd: vec!["/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img".into()],
801 options: Some("root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6".into()),
802 };
803
804 let mut config2 = BLSConfig::default();
805 config2.title = Some("Fedora 41.20250214.2.0 (CoreOS)".into());
806 config2.sort_key = Some("2".into());
807 config2.cfg_type = BLSConfigType::NonEFI {
808 linux: "/boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/vmlinuz-5.14.10".into(),
809 initrd: vec!["/boot/febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01/initramfs-5.14.10.img".into()],
810 options: Some("root=UUID=abc123 rw composefs=febdf62805de2ae7b6b597f2a9775d9c8a753ba1e5f09298fc8fbe0b0d13bf01".into())
811 };
812
813 assert_eq!(result[0].sort_key.as_ref().unwrap(), "1");
814 assert_eq!(result[1].sort_key.as_ref().unwrap(), "2");
815
816 let result = get_sorted_type1_boot_entries(&tempdir, false).unwrap();
817 assert_eq!(result[0].sort_key.as_ref().unwrap(), "2");
818 assert_eq!(result[1].sort_key.as_ref().unwrap(), "1");
819
820 Ok(())
821 }
822
823 #[test]
824 fn test_sorted_uki_boot_entries() -> Result<()> {
825 let user_cfg = r#"
826 if [ -f ${config_directory}/efiuuid.cfg ]; then
827 source ${config_directory}/efiuuid.cfg
828 fi
829
830 menuentry "Fedora Bootc UKI: (f7415d75017a12a387a39d2281e033a288fc15775108250ef70a01dcadb93346)" {
831 insmod fat
832 insmod chain
833 search --no-floppy --set=root --fs-uuid "${EFI_PART_UUID}"
834 chainloader /EFI/Linux/f7415d75017a12a387a39d2281e033a288fc15775108250ef70a01dcadb93346.efi
835 }
836
837 menuentry "Fedora Bootc UKI: (7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6)" {
838 insmod fat
839 insmod chain
840 search --no-floppy --set=root --fs-uuid "${EFI_PART_UUID}"
841 chainloader /EFI/Linux/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6.efi
842 }
843 "#;
844
845 let bootdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?;
846 bootdir.create_dir_all(format!("grub2"))?;
847 bootdir.atomic_write(format!("grub2/{USER_CFG}"), user_cfg)?;
848
849 let mut s = String::new();
850 let result = get_sorted_grub_uki_boot_entries(&bootdir, &mut s)?;
851
852 let expected = vec![
853 MenuEntry {
854 title: "Fedora Bootc UKI: (f7415d75017a12a387a39d2281e033a288fc15775108250ef70a01dcadb93346)".into(),
855 body: MenuentryBody {
856 insmod: vec!["fat", "chain"],
857 chainloader: "/EFI/Linux/f7415d75017a12a387a39d2281e033a288fc15775108250ef70a01dcadb93346.efi".into(),
858 search: "--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\"",
859 version: 0,
860 extra: vec![],
861 },
862 },
863 MenuEntry {
864 title: "Fedora Bootc UKI: (7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6)".into(),
865 body: MenuentryBody {
866 insmod: vec!["fat", "chain"],
867 chainloader: "/EFI/Linux/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6.efi".into(),
868 search: "--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\"",
869 version: 0,
870 extra: vec![],
871 },
872 },
873 ];
874
875 assert_eq!(result, expected);
876
877 Ok(())
878 }
879}