Skip to main content

cargo_image_runner/bootloader/
mod.rs

1//! Bootloader trait and built-in implementations (Limine, GRUB, none).
2
3use crate::config::BootType;
4use crate::core::context::Context;
5use crate::core::error::Result;
6use std::collections::HashMap;
7use std::path::PathBuf;
8
9// Bootloader implementations
10#[cfg(feature = "limine")]
11pub mod limine;
12
13pub mod grub;
14pub mod none;
15
16#[cfg(feature = "limine")]
17mod fetcher;
18
19#[cfg(feature = "limine")]
20pub use fetcher::GitFetcher;
21
22/// Bootloader trait for preparing boot files and configuration.
23pub trait Bootloader: Send + Sync {
24    /// Prepare bootloader files (download, extract, etc.).
25    ///
26    /// Returns the files that need to be included in the image.
27    fn prepare(&self, ctx: &Context) -> Result<BootloaderFiles>;
28
29    /// Get bootloader configuration files to include in image.
30    ///
31    /// These files may need template processing.
32    fn config_files(&self, ctx: &Context) -> Result<Vec<ConfigFile>>;
33
34    /// Process template variables in content.
35    ///
36    /// Supports both {{VAR}} and $VAR syntax.
37    fn process_templates(&self, content: &str, vars: &HashMap<String, String>) -> Result<String> {
38        let mut result = content.to_string();
39
40        // Process {{VAR}} syntax
41        for (key, value) in vars {
42            let placeholder = format!("{{{{{}}}}}", key);
43            result = result.replace(&placeholder, value);
44        }
45
46        // Process $VAR syntax (only at word boundaries)
47        for (key, value) in vars {
48            let placeholder = format!("${}", key);
49            result = result.replace(&placeholder, value);
50        }
51
52        Ok(result)
53    }
54
55    /// Get required boot type (BIOS/UEFI/both).
56    fn boot_type(&self) -> BootType;
57
58    /// Validate configuration for this bootloader.
59    fn validate_config(&self, ctx: &Context) -> Result<()> {
60        // Check boot type compatibility
61        let required = self.boot_type();
62        let configured = ctx.config.boot.boot_type;
63
64        match (required, configured) {
65            (BootType::Bios, BootType::Uefi) | (BootType::Uefi, BootType::Bios) => {
66                return Err(crate::core::error::Error::unsupported(format!(
67                    "Bootloader requires {:?} but boot type is configured as {:?}",
68                    required, configured
69                )));
70            }
71            _ => {}
72        }
73
74        Ok(())
75    }
76
77    /// Get a human-readable name for this bootloader.
78    fn name(&self) -> &str;
79}
80
81/// Files prepared by the bootloader.
82#[derive(Debug, Default)]
83pub struct BootloaderFiles {
84    /// Files for BIOS boot (e.g., boot sector, stage files).
85    pub bios_files: Vec<FileEntry>,
86
87    /// Files for UEFI boot (e.g., EFI executables).
88    pub uefi_files: Vec<FileEntry>,
89
90    /// Files that go in the root/system area of the image.
91    pub system_files: Vec<FileEntry>,
92}
93
94impl BootloaderFiles {
95    /// Create an empty set of bootloader files.
96    pub fn new() -> Self {
97        Self::default()
98    }
99
100    /// Add a BIOS file.
101    pub fn add_bios_file(mut self, source: PathBuf, dest: PathBuf) -> Self {
102        self.bios_files.push(FileEntry { source, dest });
103        self
104    }
105
106    /// Add a UEFI file.
107    pub fn add_uefi_file(mut self, source: PathBuf, dest: PathBuf) -> Self {
108        self.uefi_files.push(FileEntry { source, dest });
109        self
110    }
111
112    /// Add a system file.
113    pub fn add_system_file(mut self, source: PathBuf, dest: PathBuf) -> Self {
114        self.system_files.push(FileEntry { source, dest });
115        self
116    }
117}
118
119/// Configuration file that may need processing.
120#[derive(Debug, Clone)]
121pub struct ConfigFile {
122    /// Source path (template).
123    pub source: PathBuf,
124
125    /// Destination path in the image.
126    pub dest: PathBuf,
127
128    /// Whether this file needs template variable substitution.
129    pub needs_template_processing: bool,
130}
131
132impl ConfigFile {
133    /// Create a new config file entry.
134    pub fn new(source: PathBuf, dest: PathBuf) -> Self {
135        Self {
136            source,
137            dest,
138            needs_template_processing: false,
139        }
140    }
141
142    /// Mark this file as needing template processing.
143    pub fn with_template_processing(mut self) -> Self {
144        self.needs_template_processing = true;
145        self
146    }
147}
148
149/// File entry for inclusion in the image.
150#[derive(Debug, Clone)]
151pub struct FileEntry {
152    /// Source path on the host filesystem.
153    pub source: PathBuf,
154
155    /// Destination path in the image.
156    pub dest: PathBuf,
157}
158
159impl FileEntry {
160    /// Create a new file entry.
161    pub fn new(source: PathBuf, dest: PathBuf) -> Self {
162        Self { source, dest }
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_bootloader_files_new_is_empty() {
172        let files = BootloaderFiles::new();
173        assert!(files.bios_files.is_empty());
174        assert!(files.uefi_files.is_empty());
175        assert!(files.system_files.is_empty());
176    }
177
178    #[test]
179    fn test_bootloader_files_builder_chain() {
180        let files = BootloaderFiles::new()
181            .add_bios_file(PathBuf::from("bios.sys"), PathBuf::from("boot/bios.sys"))
182            .add_uefi_file(PathBuf::from("bootx64.efi"), PathBuf::from("efi/boot/bootx64.efi"))
183            .add_system_file(PathBuf::from("kernel.elf"), PathBuf::from("boot/kernel.elf"));
184
185        assert_eq!(files.bios_files.len(), 1);
186        assert_eq!(files.uefi_files.len(), 1);
187        assert_eq!(files.system_files.len(), 1);
188        assert_eq!(files.bios_files[0].source, PathBuf::from("bios.sys"));
189        assert_eq!(files.uefi_files[0].dest, PathBuf::from("efi/boot/bootx64.efi"));
190        assert_eq!(files.system_files[0].source, PathBuf::from("kernel.elf"));
191    }
192
193    #[test]
194    fn test_config_file_template_processing() {
195        let cf = ConfigFile::new(PathBuf::from("limine.conf"), PathBuf::from("boot/limine.conf"));
196        assert!(!cf.needs_template_processing);
197
198        let cf = cf.with_template_processing();
199        assert!(cf.needs_template_processing);
200        assert_eq!(cf.source, PathBuf::from("limine.conf"));
201        assert_eq!(cf.dest, PathBuf::from("boot/limine.conf"));
202    }
203
204    #[test]
205    fn test_file_entry_construction() {
206        let entry = FileEntry::new(PathBuf::from("/src/kernel"), PathBuf::from("boot/kernel"));
207        assert_eq!(entry.source, PathBuf::from("/src/kernel"));
208        assert_eq!(entry.dest, PathBuf::from("boot/kernel"));
209    }
210
211    #[test]
212    fn test_process_templates_default_impl() {
213        let bootloader = none::NoneBootloader::new();
214        let mut vars = HashMap::new();
215        vars.insert("KEY".to_string(), "value".to_string());
216
217        let result = bootloader.process_templates("Hello {{KEY}}", &vars).unwrap();
218        assert_eq!(result, "Hello value");
219    }
220}