1use core::ops::Range;
10use std::{
11 collections::HashMap, ffi::OsStr, os::unix::ffi::OsStrExt, path::PathBuf, str::from_utf8,
12};
13
14use anyhow::{bail, Result};
15
16use composefs::{
17 fsverity::FsVerityHashValue,
18 repository::Repository,
19 tree::{Directory, FileSystem, ImageError, Inode, LeafContent, RegularFile},
20};
21
22use crate::cmdline::{make_cmdline_composefs, split_cmdline};
23
24fn strip_ble_key<'a>(line: &'a str, key: &str) -> Option<&'a str> {
31 let rest = line.strip_prefix(key)?;
32 if !rest.chars().next()?.is_ascii_whitespace() {
33 return None;
34 }
35 Some(rest.trim_start())
36}
37
38fn substr_range(parent: &str, substr: &str) -> Option<Range<usize>> {
40 let parent_start = parent as *const str as *const u8 as usize;
41 let parent_end = parent_start + parent.len();
42 let substr_start = substr as *const str as *const u8 as usize;
43 let substr_end = substr_start + substr.len();
44
45 if parent_start <= substr_start && substr_end <= parent_end {
46 Some((substr_start - parent_start)..(substr_end - parent_start))
47 } else {
48 None
49 }
50}
51
52#[derive(Debug)]
57pub struct BootLoaderEntryFile {
58 pub lines: Vec<String>,
60}
61
62impl BootLoaderEntryFile {
63 pub fn new(content: &str) -> Self {
73 Self {
74 lines: content.split_terminator('\n').map(String::from).collect(),
75 }
76 }
77
78 pub fn get_values<'a>(&'a self, key: &'a str) -> impl Iterator<Item = &'a str> + 'a {
88 self.lines
89 .iter()
90 .filter_map(|line| strip_ble_key(line, key))
91 }
92
93 pub fn get_value(&self, key: &str) -> Option<&str> {
103 self.lines.iter().find_map(|line| strip_ble_key(line, key))
104 }
105
106 pub fn add_cmdline(&mut self, arg: &str) {
111 let key = match arg.find('=') {
112 Some(pos) => &arg[..=pos], None => arg,
114 };
115
116 for line in &mut self.lines {
121 if let Some(cmdline) = strip_ble_key(line, "options") {
122 let segment = split_cmdline(cmdline).find(|s| s.starts_with(key));
123
124 if let Some(old) = segment {
125 let range = substr_range(line, old).unwrap();
127 line.replace_range(range, arg);
128 } else {
129 line.push(' ');
131 line.push_str(arg);
132 }
133
134 return;
135 }
136 }
137
138 self.lines.push(format!("options {arg}"));
140 }
141
142 pub fn adjust_cmdline(&mut self, composefs: Option<&str>, insecure: bool, extra: &[&str]) {
145 if let Some(id) = composefs {
146 self.add_cmdline(&make_cmdline_composefs(id, insecure));
147 }
148
149 for item in extra {
150 self.add_cmdline(item);
151 }
152 }
153}
154
155#[derive(Debug)]
160pub struct Type1Entry<ObjectID: FsVerityHashValue> {
161 pub filename: Box<OsStr>,
163 pub entry: BootLoaderEntryFile,
165 pub files: HashMap<Box<str>, RegularFile<ObjectID>>,
167}
168
169impl<ObjectID: FsVerityHashValue> Type1Entry<ObjectID> {
170 pub fn relocate(&mut self, boot_subdir: Option<&str>, entry_id: &str) {
181 self.filename = Box::from(format!("{entry_id}.conf").as_ref());
182 for line in &mut self.entry.lines {
183 for key in ["linux", "initrd", "efi"] {
184 let Some(value) = strip_ble_key(line, key) else {
185 continue;
186 };
187 let Some((_dir, basename)) = value.rsplit_once("/") else {
188 continue;
189 };
190
191 let file = self.files.remove(value);
192
193 let new = format!("/{entry_id}/{basename}");
194 let range = substr_range(line, value).unwrap();
195
196 let final_entry_path = if let Some(boot_subdir) = boot_subdir {
197 format!("/{boot_subdir}{new}")
198 } else {
199 new.clone()
200 };
201
202 line.replace_range(range, &final_entry_path);
203
204 if let Some(file) = file {
205 self.files.insert(new.into_boxed_str(), file);
206 }
207 }
208 }
209 }
210
211 pub fn load(
227 filename: &OsStr,
228 file: &RegularFile<ObjectID>,
229 root: &Directory<ObjectID>,
230 repo: &Repository<ObjectID>,
231 ) -> Result<Self> {
232 let entry = BootLoaderEntryFile::new(from_utf8(&composefs::fs::read_file(file, repo)?)?);
233
234 let mut files = HashMap::new();
235 for key in ["linux", "initrd", "efi"] {
236 for pathname in entry.get_values(key) {
237 let (dir, filename) = root.split(pathname.as_ref())?;
238 files.insert(Box::from(pathname), dir.get_file(filename)?.clone());
239 }
240 }
241
242 Ok(Self {
243 filename: Box::from(filename),
244 entry,
245 files,
246 })
247 }
248
249 pub fn load_all(root: &Directory<ObjectID>, repo: &Repository<ObjectID>) -> Result<Vec<Self>> {
260 let mut entries = vec![];
261
262 match root.get_directory("/boot/loader/entries".as_ref()) {
263 Ok(entries_dir) => {
264 for (filename, inode) in entries_dir.entries() {
265 if !filename.as_bytes().ends_with(b".conf") {
266 continue;
267 }
268
269 let Inode::Leaf(leaf) = inode else {
270 bail!("/boot/loader/entries/{filename:?} is a directory");
271 };
272
273 let LeafContent::Regular(file) = &leaf.content else {
274 bail!("/boot/loader/entries/{filename:?} is not a regular file");
275 };
276
277 entries.push(Self::load(filename, file, root, repo)?);
278 }
279 }
280 Err(ImageError::NotFound(..)) => {}
281 Err(other) => Err(other)?,
282 };
283
284 Ok(entries)
285 }
286}
287
288pub const EFI_EXT: &str = ".efi";
290pub const EFI_ADDON_DIR_EXT: &str = ".efi.extra.d";
292pub const EFI_ADDON_FILE_EXT: &str = ".addon.efi";
294
295#[derive(Debug)]
297pub enum PEType {
298 Uki,
300 UkiAddon,
302}
303
304#[derive(Debug)]
309pub struct Type2Entry<ObjectID: FsVerityHashValue> {
310 pub kver: Option<Box<OsStr>>,
312 pub file_path: PathBuf,
314 pub file: RegularFile<ObjectID>,
316 pub pe_type: PEType,
318}
319
320impl<ObjectID: FsVerityHashValue> Type2Entry<ObjectID> {
321 pub fn rename(&mut self, name: &str) {
327 let new_name = format!("{name}.efi");
328
329 if let Some(parent) = self.file_path.parent() {
330 self.file_path = parent.join(new_name);
331 } else {
332 self.file_path = new_name.into();
333 }
334 }
335
336 fn find_uki_components(
339 dir: &Directory<ObjectID>,
340 entries: &mut Vec<Self>,
341 path: &mut PathBuf,
342 kver: &Option<Box<OsStr>>,
343 ) -> Result<()> {
344 for (filename, inode) in dir.entries() {
345 path.push(filename);
346
347 if let Inode::Directory(dir) = inode {
351 Self::find_uki_components(dir, entries, path, kver)?;
352 path.pop();
353 continue;
354 }
355
356 if !filename.as_bytes().ends_with(EFI_EXT.as_bytes()) {
357 path.pop();
358 continue;
359 }
360
361 let Inode::Leaf(leaf) = inode else {
362 bail!("{filename:?} is a directory");
363 };
364
365 let LeafContent::Regular(file) = &leaf.content else {
366 bail!("{filename:?} is not a regular file");
367 };
368
369 entries.push(Self {
370 kver: kver.clone(),
371 file_path: path.clone(),
372 file: file.clone(),
373 pe_type: if path.components().count() == 1 {
374 PEType::Uki
375 } else {
376 PEType::UkiAddon
377 },
378 });
379
380 path.pop();
381 }
382
383 Ok(())
384 }
385
386 pub fn load_all(root: &Directory<ObjectID>) -> Result<Vec<Self>> {
396 let mut entries = vec![];
397
398 match root.get_directory("/boot/EFI/Linux".as_ref()) {
399 Ok(entries_dir) => {
400 Self::find_uki_components(entries_dir, &mut entries, &mut PathBuf::new(), &None)?
401 }
402 Err(ImageError::NotFound(..)) => {}
403 Err(other) => Err(other)?,
404 };
405
406 match root.get_directory("/usr/lib/modules".as_ref()) {
407 Ok(modules_dir) => {
408 for (kver, inode) in modules_dir.entries() {
409 let Inode::Directory(dir) = inode else {
410 continue;
411 };
412
413 Self::find_uki_components(
414 dir,
415 &mut entries,
416 &mut PathBuf::new(),
417 &Some(Box::from(kver)),
418 )?;
419 }
420 }
421 Err(ImageError::NotFound(..)) => {}
422 Err(other) => Err(other)?,
423 };
424
425 Ok(entries)
426 }
427}
428
429#[derive(Debug)]
434pub struct UsrLibModulesVmlinuz<ObjectID: FsVerityHashValue> {
435 pub kver: Box<str>,
437 pub vmlinuz: RegularFile<ObjectID>,
439 pub initramfs: Option<RegularFile<ObjectID>>,
441 pub os_release: Option<RegularFile<ObjectID>>,
443}
444
445impl<ObjectID: FsVerityHashValue> UsrLibModulesVmlinuz<ObjectID> {
446 pub fn into_type1(self, entry_id: Option<&str>) -> Type1Entry<ObjectID> {
456 let id = entry_id.unwrap_or(&self.kver);
457
458 let title = "todoOS";
459 let version = "0-todo";
460 let entry = BootLoaderEntryFile::new(&format!(
461 r#"# File created by composefs
462title {title}
463version {version}
464linux /{id}/vmlinuz
465initrd /{id}/initramfs.img
466"#
467 ));
468
469 let filename = Box::from(format!("{id}.conf").as_ref());
470
471 Type1Entry {
472 filename,
473 entry,
474 files: HashMap::from([
475 (Box::from(format!("/{id}/vmlinuz")), self.vmlinuz),
476 (
477 Box::from(format!("/{id}/initramfs.img")),
478 self.initramfs.unwrap(),
479 ),
480 ]),
481 }
482 }
483
484 pub fn load_all(root: &Directory<ObjectID>) -> Result<Vec<Self>> {
494 let mut entries = vec![];
495
496 match root.get_directory("/usr/lib/modules".as_ref()) {
497 Ok(modules_dir) => {
498 for (kver, inode) in modules_dir.entries() {
499 let Inode::Directory(dir) = inode else {
500 continue;
501 };
502
503 if let Ok(vmlinuz) = dir.get_file("vmlinuz".as_ref()) {
504 let initramfs = dir.get_file("initramfs.img".as_ref()).ok();
507 let os_release = root.get_file("/usr/lib/os-release".as_ref()).ok();
508 entries.push(Self {
509 kver: Box::from(std::str::from_utf8(kver.as_bytes())?),
510 vmlinuz: vmlinuz.clone(),
511 initramfs: initramfs.cloned(),
512 os_release: os_release.cloned(),
513 });
514 }
515 }
516 }
517 Err(ImageError::NotFound(..)) => {}
518 Err(other) => Err(other)?,
519 };
520
521 Ok(entries)
522 }
523}
524
525#[derive(Debug)]
530pub enum BootEntry<ObjectID: FsVerityHashValue> {
531 Type1(Type1Entry<ObjectID>),
533 Type2(Type2Entry<ObjectID>),
535 UsrLibModulesVmLinuz(UsrLibModulesVmlinuz<ObjectID>),
537}
538
539pub fn get_boot_resources<ObjectID: FsVerityHashValue>(
554 image: &FileSystem<ObjectID>,
555 repo: &Repository<ObjectID>,
556) -> Result<Vec<BootEntry<ObjectID>>> {
557 let mut entries = vec![];
558
559 for e in Type1Entry::load_all(&image.root, repo)? {
560 entries.push(BootEntry::Type1(e));
561 }
562 for e in Type2Entry::load_all(&image.root)? {
563 entries.push(BootEntry::Type2(e));
564 }
565 for e in UsrLibModulesVmlinuz::load_all(&image.root)? {
566 entries.push(BootEntry::UsrLibModulesVmLinuz(e));
567 }
568
569 Ok(entries)
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575
576 #[test]
577 fn test_bootloader_entry_file_new() {
578 let content = "title Test Entry\nversion 1.0\nlinux /vmlinuz\ninitrd /initramfs.img\noptions quiet splash\n";
579 let entry = BootLoaderEntryFile::new(content);
580
581 assert_eq!(entry.lines.len(), 5);
582 assert_eq!(entry.lines[0], "title Test Entry");
583 assert_eq!(entry.lines[1], "version 1.0");
584 assert_eq!(entry.lines[2], "linux /vmlinuz");
585 assert_eq!(entry.lines[3], "initrd /initramfs.img");
586 assert_eq!(entry.lines[4], "options quiet splash");
587 }
588
589 #[test]
590 fn test_bootloader_entry_file_new_empty() {
591 let entry = BootLoaderEntryFile::new("");
592 assert_eq!(entry.lines.len(), 0);
593 }
594
595 #[test]
596 fn test_bootloader_entry_file_new_single_line() {
597 let entry = BootLoaderEntryFile::new("title Test");
598 assert_eq!(entry.lines.len(), 1);
599 assert_eq!(entry.lines[0], "title Test");
600 }
601
602 #[test]
603 fn test_bootloader_entry_file_new_trailing_newline() {
604 let content = "title Test\nversion 1.0\n";
605 let entry = BootLoaderEntryFile::new(content);
606 assert_eq!(entry.lines.len(), 2);
607 assert_eq!(entry.lines[0], "title Test");
608 assert_eq!(entry.lines[1], "version 1.0");
609 }
610
611 #[test]
612 fn test_get_value() {
613 let content = "title Test Entry\nversion 1.0\nlinux /vmlinuz\ninitrd /initramfs.img\noptions quiet splash\n";
614 let entry = BootLoaderEntryFile::new(content);
615
616 assert_eq!(entry.get_value("title"), Some("Test Entry"));
617 assert_eq!(entry.get_value("version"), Some("1.0"));
618 assert_eq!(entry.get_value("linux"), Some("/vmlinuz"));
619 assert_eq!(entry.get_value("initrd"), Some("/initramfs.img"));
620 assert_eq!(entry.get_value("options"), Some("quiet splash"));
621 assert_eq!(entry.get_value("nonexistent"), None);
622 }
623
624 #[test]
625 fn test_get_value_whitespace_handling() {
626 let content = "title\t\tTest Entry\nversion 1.0\nlinux\t/vmlinuz\n";
627 let entry = BootLoaderEntryFile::new(content);
628
629 assert_eq!(entry.get_value("title"), Some("Test Entry"));
630 assert_eq!(entry.get_value("version"), Some("1.0"));
631 assert_eq!(entry.get_value("linux"), Some("/vmlinuz"));
632 }
633
634 #[test]
635 fn test_get_value_no_whitespace_after_key() {
636 let content = "titleTest Entry\nversionno_space\n";
637 let entry = BootLoaderEntryFile::new(content);
638
639 assert_eq!(entry.get_value("title"), None);
640 assert_eq!(entry.get_value("version"), None);
641 }
642
643 #[test]
644 fn test_get_values_multiple() {
645 let content = "title Test Entry\ninitrd /initramfs1.img\ninitrd /initramfs2.img\noptions quiet\noptions splash\n";
646 let entry = BootLoaderEntryFile::new(content);
647
648 let initrd_values: Vec<_> = entry.get_values("initrd").collect();
649 assert_eq!(initrd_values, vec!["/initramfs1.img", "/initramfs2.img"]);
650
651 let options_values: Vec<_> = entry.get_values("options").collect();
652 assert_eq!(options_values, vec!["quiet", "splash"]);
653
654 let title_values: Vec<_> = entry.get_values("title").collect();
655 assert_eq!(title_values, vec!["Test Entry"]);
656
657 let nonexistent_values: Vec<_> = entry.get_values("nonexistent").collect();
658 assert_eq!(nonexistent_values, Vec::<&str>::new());
659 }
660
661 #[test]
662 fn test_add_cmdline_new_options_line() {
663 let mut entry = BootLoaderEntryFile::new("title Test Entry\nlinux /vmlinuz\n");
664 entry.add_cmdline("quiet");
665
666 assert_eq!(entry.lines.len(), 3);
667 assert_eq!(entry.lines[2], "options quiet");
668 }
669
670 #[test]
671 fn test_add_cmdline_append_to_existing_options() {
672 let mut entry = BootLoaderEntryFile::new("title Test Entry\noptions splash\n");
673 entry.add_cmdline("quiet");
674
675 assert_eq!(entry.lines.len(), 2);
676 assert_eq!(entry.lines[1], "options splash quiet");
677 }
678
679 #[test]
680 fn test_add_cmdline_replace_existing_key_value() {
681 let mut entry =
682 BootLoaderEntryFile::new("title Test Entry\noptions quiet splash root=/dev/sda1\n");
683 entry.add_cmdline("root=/dev/sda2");
684
685 assert_eq!(entry.lines.len(), 2);
686 assert_eq!(entry.lines[1], "options quiet splash root=/dev/sda2");
687 }
688
689 #[test]
690 fn test_add_cmdline_replace_existing_key_only() {
691 let mut entry = BootLoaderEntryFile::new("title Test Entry\noptions quiet rw splash\n");
692 entry.add_cmdline("rw"); assert_eq!(entry.lines.len(), 2);
695 assert_eq!(entry.lines[1], "options quiet rw splash");
696
697 entry.add_cmdline("ro");
699 assert_eq!(entry.lines[1], "options quiet rw splash ro");
700 }
701
702 #[test]
703 fn test_add_cmdline_key_with_equals() {
704 let mut entry = BootLoaderEntryFile::new("title Test Entry\noptions quiet\n");
705 entry.add_cmdline("composefs=abc123");
706
707 assert_eq!(entry.lines.len(), 2);
708 assert_eq!(entry.lines[1], "options quiet composefs=abc123");
709 }
710
711 #[test]
712 fn test_add_cmdline_replace_key_with_equals() {
713 let mut entry =
714 BootLoaderEntryFile::new("title Test Entry\noptions quiet composefs=old123\n");
715 entry.add_cmdline("composefs=new456");
716
717 assert_eq!(entry.lines.len(), 2);
718 assert_eq!(entry.lines[1], "options quiet composefs=new456");
719 }
720
721 #[test]
722 fn test_adjust_cmdline_with_composefs() {
723 let mut entry = BootLoaderEntryFile::new("title Test Entry\nlinux /vmlinuz\n");
724 entry.adjust_cmdline(Some("abc123"), false, &["quiet", "splash"]);
725
726 assert_eq!(entry.lines.len(), 3);
727 assert_eq!(entry.lines[2], "options composefs=abc123 quiet splash");
728 }
729
730 #[test]
731 fn test_adjust_cmdline_with_composefs_insecure() {
732 let mut entry = BootLoaderEntryFile::new("title Test Entry\nlinux /vmlinuz\n");
733 entry.adjust_cmdline(Some("abc123"), true, &[]);
734
735 assert_eq!(entry.lines.len(), 3);
736 assert!(entry.lines[2].contains("abc123"));
738 }
739
740 #[test]
741 fn test_adjust_cmdline_no_composefs() {
742 let mut entry = BootLoaderEntryFile::new("title Test Entry\nlinux /vmlinuz\n");
743 entry.adjust_cmdline(None, false, &["quiet", "splash"]);
744
745 assert_eq!(entry.lines.len(), 3);
746 assert_eq!(entry.lines[2], "options quiet splash");
747 }
748
749 #[test]
750 fn test_adjust_cmdline_existing_options() {
751 let mut entry = BootLoaderEntryFile::new("title Test Entry\noptions root=/dev/sda1\n");
752 entry.adjust_cmdline(Some("abc123"), false, &["quiet"]);
753
754 assert_eq!(entry.lines.len(), 2);
755 assert!(entry.lines[1].contains("root=/dev/sda1"));
756 assert!(entry.lines[1].contains("abc123"));
757 assert!(entry.lines[1].contains("quiet"));
758 }
759
760 #[test]
761 fn test_strip_ble_key_helper() {
762 assert_eq!(
763 strip_ble_key("title Test Entry", "title"),
764 Some("Test Entry")
765 );
766 assert_eq!(
767 strip_ble_key("title\tTest Entry", "title"),
768 Some("Test Entry")
769 );
770 assert_eq!(
771 strip_ble_key("title Test Entry", "title"),
772 Some("Test Entry")
773 );
774 assert_eq!(strip_ble_key("titleTest Entry", "title"), None);
775 assert_eq!(strip_ble_key("other Test Entry", "title"), None);
776 assert_eq!(strip_ble_key("title", "title"), None); }
778
779 #[test]
780 fn test_substr_range_helper() {
781 let parent = "hello world test";
782 let substr = &parent[6..11]; let range = substr_range(parent, substr).unwrap();
784 assert_eq!(range, 6..11);
785 assert_eq!(&parent[range], "world");
786
787 let other_substr = &parent[0..5]; let range2 = substr_range(parent, other_substr).unwrap();
790 assert_eq!(range2, 0..5);
791 assert_eq!(&parent[range2], "hello");
792
793 let separate_string = String::from("world");
795 assert_eq!(substr_range(parent, &separate_string), None);
796 }
797}