1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::PathBuf;
6
7pub mod env;
8mod loader;
9pub use loader::ConfigLoader;
10
11#[derive(Debug, Clone, Serialize, Deserialize, Default)]
13pub struct Config {
14 #[serde(default)]
16 pub boot: BootConfig,
17
18 #[serde(default)]
20 pub bootloader: BootloaderConfig,
21
22 #[serde(default)]
24 pub image: ImageConfig,
25
26 #[serde(default)]
28 pub runner: RunnerConfig,
29
30 #[serde(default)]
32 pub test: TestConfig,
33
34 #[serde(default)]
36 pub run: RunConfig,
37
38 #[serde(default)]
40 pub variables: HashMap<String, String>,
41
42 #[serde(default, rename = "extra-files")]
44 pub extra_files: HashMap<String, String>,
45
46 #[serde(default)]
48 pub verbose: bool,
49}
50
51impl Config {
52 pub fn from_toml_str(toml: &str) -> crate::core::Result<Self> {
57 toml::from_str(toml).map_err(|e| crate::core::Error::config(format!("failed to parse TOML config: {}", e)))
58 }
59
60 pub fn from_toml_file(path: impl AsRef<std::path::Path>) -> crate::core::Result<Self> {
65 let content = std::fs::read_to_string(path.as_ref())
66 .map_err(|e| crate::core::Error::config(format!("failed to read config file: {}", e)))?;
67 Self::from_toml_str(&content)
68 }
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct BootConfig {
74 #[serde(rename = "type")]
76 pub boot_type: BootType,
77}
78
79impl Default for BootConfig {
80 fn default() -> Self {
81 Self {
82 boot_type: BootType::Uefi,
83 }
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "lowercase")]
90pub enum BootType {
91 Bios,
93 Uefi,
95 Hybrid,
97}
98
99impl BootType {
100 pub fn needs_bios(self) -> bool {
102 matches!(self, BootType::Bios | BootType::Hybrid)
103 }
104
105 pub fn needs_uefi(self) -> bool {
107 matches!(self, BootType::Uefi | BootType::Hybrid)
108 }
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize, Default)]
113pub struct BootloaderConfig {
114 pub kind: BootloaderKind,
116
117 #[serde(rename = "config-file")]
119 pub config_file: Option<PathBuf>,
120
121 #[serde(default)]
123 pub limine: LimineConfig,
124
125 #[serde(default)]
127 pub grub: GrubConfig,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
132#[serde(rename_all = "lowercase")]
133pub enum BootloaderKind {
134 Limine,
136 Grub,
138 #[default]
140 None,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct LimineConfig {
146 pub version: String,
148}
149
150impl Default for LimineConfig {
151 fn default() -> Self {
152 Self {
153 version: "v8.x-binary".to_string(),
154 }
155 }
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize, Default)]
160pub struct GrubConfig {
161 #[serde(default)]
163 pub modules: Vec<String>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct ImageConfig {
169 pub format: ImageFormat,
171
172 pub output: Option<PathBuf>,
174
175 #[serde(default = "default_volume_label")]
177 pub volume_label: String,
178}
179
180impl Default for ImageConfig {
181 fn default() -> Self {
182 Self {
183 format: ImageFormat::Directory,
184 output: None,
185 volume_label: default_volume_label(),
186 }
187 }
188}
189
190fn default_volume_label() -> String {
191 "BOOT".to_string()
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
196#[serde(rename_all = "lowercase")]
197pub enum ImageFormat {
198 Iso,
200 Fat,
202 #[default]
204 Directory,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize, Default)]
209pub struct RunnerConfig {
210 pub kind: RunnerKind,
212
213 #[serde(default)]
215 pub qemu: QemuConfig,
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
220#[serde(rename_all = "lowercase")]
221pub enum RunnerKind {
222 #[default]
224 Qemu,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229#[serde(default)]
230pub struct QemuConfig {
231 #[serde(default = "default_qemu_binary")]
233 pub binary: String,
234
235 #[serde(default = "default_machine")]
237 pub machine: String,
238
239 #[serde(default = "default_memory")]
241 pub memory: u32,
242
243 #[serde(default = "default_cores")]
245 pub cores: u32,
246
247 #[serde(default = "default_true")]
249 pub kvm: bool,
250
251 #[serde(default)]
253 pub serial: SerialConfig,
254
255 #[serde(default)]
257 pub extra_args: Vec<String>,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262#[serde(default)]
263pub struct SerialConfig {
264 pub mode: SerialMode,
266 #[serde(default, rename = "separate-monitor")]
269 pub separate_monitor: Option<bool>,
270}
271
272impl Default for SerialConfig {
273 fn default() -> Self {
274 Self {
275 mode: SerialMode::default(),
276 separate_monitor: None,
277 }
278 }
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
283pub enum SerialMode {
284 #[default]
286 #[serde(rename = "mon:stdio")]
287 MonStdio,
288 #[serde(rename = "stdio")]
290 Stdio,
291 #[serde(rename = "none")]
293 None,
294}
295
296fn default_qemu_binary() -> String {
297 "qemu-system-x86_64".to_string()
298}
299
300fn default_machine() -> String {
301 "q35".to_string()
302}
303
304fn default_memory() -> u32 {
305 1024
306}
307
308fn default_cores() -> u32 {
309 1
310}
311
312impl Default for QemuConfig {
313 fn default() -> Self {
314 Self {
315 binary: "qemu-system-x86_64".to_string(),
316 machine: "q35".to_string(),
317 memory: 1024,
318 cores: 1,
319 kvm: true,
320 serial: SerialConfig::default(),
321 extra_args: Vec::new(),
322 }
323 }
324}
325
326fn default_true() -> bool {
327 true
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize, Default)]
332pub struct TestConfig {
333 #[serde(rename = "success-exit-code")]
335 pub success_exit_code: Option<i32>,
336
337 #[serde(default, rename = "extra-args")]
339 pub extra_args: Vec<String>,
340
341 pub timeout: Option<u64>,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize, Default)]
347pub struct RunConfig {
348 #[serde(default, rename = "extra-args")]
350 pub extra_args: Vec<String>,
351
352 #[serde(default)]
354 pub gui: bool,
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
362 fn test_config_default_values() {
363 let config = Config::default();
364 assert_eq!(config.boot.boot_type, BootType::Uefi);
365 assert_eq!(config.bootloader.kind, BootloaderKind::None);
366 assert!(config.bootloader.config_file.is_none());
367 assert_eq!(config.image.format, ImageFormat::Directory);
368 assert!(config.image.output.is_none());
369 assert_eq!(config.image.volume_label, "BOOT");
370 assert_eq!(config.runner.kind, RunnerKind::Qemu);
371 assert!(config.test.success_exit_code.is_none());
372 assert!(config.test.extra_args.is_empty());
373 assert!(config.test.timeout.is_none());
374 assert!(!config.run.gui);
375 assert!(config.run.extra_args.is_empty());
376 assert!(config.variables.is_empty());
377 assert!(config.extra_files.is_empty());
378 assert!(!config.verbose);
379 }
380
381 #[test]
382 fn test_config_deserialize_minimal() {
383 let toml_str = r#"
384 [boot]
385 type = "uefi"
386 "#;
387 let config: Config = toml::from_str(toml_str).unwrap();
388 assert_eq!(config.boot.boot_type, BootType::Uefi);
389 assert_eq!(config.bootloader.kind, BootloaderKind::None);
390 assert_eq!(config.image.format, ImageFormat::Directory);
391 }
392
393 #[test]
394 fn test_config_deserialize_full() {
395 let toml_str = r#"
396 verbose = true
397
398 [boot]
399 type = "hybrid"
400
401 [bootloader]
402 kind = "limine"
403 config-file = "limine.conf"
404
405 [bootloader.limine]
406 version = "v8.4.0-binary"
407
408 [image]
409 format = "iso"
410 output = "my-os.iso"
411 volume_label = "MYOS"
412
413 [runner]
414 kind = "qemu"
415
416 [runner.qemu]
417 binary = "qemu-system-x86_64"
418 memory = 2048
419 cores = 2
420 kvm = false
421
422 [test]
423 success-exit-code = 33
424 timeout = 30
425 extra-args = ["-device", "isa-debug-exit"]
426
427 [run]
428 gui = true
429 extra-args = ["-serial", "stdio"]
430
431 [variables]
432 TIMEOUT = "5"
433
434 [extra-files]
435 "boot/extra.bin" = "extra.bin"
436 "#;
437 let config: Config = toml::from_str(toml_str).unwrap();
438 assert_eq!(config.boot.boot_type, BootType::Hybrid);
439 assert_eq!(config.bootloader.kind, BootloaderKind::Limine);
440 assert_eq!(
441 config.bootloader.config_file,
442 Some(PathBuf::from("limine.conf"))
443 );
444 assert_eq!(config.bootloader.limine.version, "v8.4.0-binary");
445 assert_eq!(config.image.format, ImageFormat::Iso);
446 assert_eq!(config.image.output, Some(PathBuf::from("my-os.iso")));
447 assert_eq!(config.image.volume_label, "MYOS");
448 assert_eq!(config.runner.qemu.memory, 2048);
449 assert_eq!(config.runner.qemu.cores, 2);
450 assert!(!config.runner.qemu.kvm);
451 assert_eq!(config.test.success_exit_code, Some(33));
452 assert_eq!(config.test.timeout, Some(30));
453 assert!(config.run.gui);
454 assert_eq!(config.variables.get("TIMEOUT").unwrap(), "5");
455 assert_eq!(config.extra_files.get("boot/extra.bin").unwrap(), "extra.bin");
456 assert!(config.verbose);
457 }
458
459 #[test]
460 fn test_config_deserialize_bios_boot_type() {
461 let toml_str = r#"
462 [boot]
463 type = "bios"
464
465 [bootloader]
466 kind = "grub"
467
468 [image]
469 format = "fat"
470 "#;
471 let config: Config = toml::from_str(toml_str).unwrap();
472 assert_eq!(config.boot.boot_type, BootType::Bios);
473 assert_eq!(config.bootloader.kind, BootloaderKind::Grub);
474 assert_eq!(config.image.format, ImageFormat::Fat);
475 }
476
477 #[test]
478 fn test_config_deserialize_invalid_boot_type() {
479 let toml_str = r#"
480 [boot]
481 type = "invalid"
482 "#;
483 let result: std::result::Result<Config, _> = toml::from_str(toml_str);
484 assert!(result.is_err());
485 }
486
487 #[test]
488 fn test_boot_type_needs_bios() {
489 assert!(BootType::Bios.needs_bios());
490 assert!(!BootType::Uefi.needs_bios());
491 assert!(BootType::Hybrid.needs_bios());
492 }
493
494 #[test]
495 fn test_boot_type_needs_uefi() {
496 assert!(!BootType::Bios.needs_uefi());
497 assert!(BootType::Uefi.needs_uefi());
498 assert!(BootType::Hybrid.needs_uefi());
499 }
500
501 #[test]
502 fn test_qemu_config_defaults() {
503 let qemu = QemuConfig::default();
504 assert_eq!(qemu.binary, "qemu-system-x86_64");
505 assert_eq!(qemu.machine, "q35");
506 assert_eq!(qemu.memory, 1024);
507 assert_eq!(qemu.cores, 1);
508 assert!(qemu.kvm);
509 assert_eq!(qemu.serial.mode, SerialMode::MonStdio);
510 assert_eq!(qemu.serial.separate_monitor, None);
511 assert!(qemu.extra_args.is_empty());
512 }
513
514 #[test]
515 fn test_serial_config_defaults() {
516 let serial = SerialConfig::default();
517 assert_eq!(serial.mode, SerialMode::MonStdio);
518 assert_eq!(serial.separate_monitor, None);
519 }
520
521 #[test]
522 fn test_serial_config_deserialize_stdio() {
523 let toml_str = r#"
524 [runner]
525 kind = "qemu"
526
527 [runner.qemu.serial]
528 mode = "stdio"
529 separate-monitor = true
530 "#;
531 let config: Config = toml::from_str(toml_str).unwrap();
532 assert_eq!(config.runner.qemu.serial.mode, SerialMode::Stdio);
533 assert_eq!(config.runner.qemu.serial.separate_monitor, Some(true));
534 }
535
536 #[test]
537 fn test_serial_config_deserialize_none() {
538 let toml_str = r#"
539 [runner]
540 kind = "qemu"
541
542 [runner.qemu.serial]
543 mode = "none"
544 "#;
545 let config: Config = toml::from_str(toml_str).unwrap();
546 assert_eq!(config.runner.qemu.serial.mode, SerialMode::None);
547 assert_eq!(config.runner.qemu.serial.separate_monitor, None);
548 }
549
550 #[test]
551 fn test_serial_config_deserialize_mon_stdio() {
552 let toml_str = r#"
553 [runner]
554 kind = "qemu"
555
556 [runner.qemu.serial]
557 mode = "mon:stdio"
558 "#;
559 let config: Config = toml::from_str(toml_str).unwrap();
560 assert_eq!(config.runner.qemu.serial.mode, SerialMode::MonStdio);
561 }
562
563 #[test]
564 fn test_serial_config_omitted_uses_defaults() {
565 let toml_str = r#"
566 [runner]
567 kind = "qemu"
568
569 [runner.qemu]
570 memory = 2048
571 "#;
572 let config: Config = toml::from_str(toml_str).unwrap();
573 assert_eq!(config.runner.qemu.serial.mode, SerialMode::MonStdio);
574 assert_eq!(config.runner.qemu.serial.separate_monitor, None);
575 }
576
577 #[test]
578 fn test_limine_config_default_version() {
579 let limine = LimineConfig::default();
580 assert_eq!(limine.version, "v8.x-binary");
581 }
582
583 #[test]
584 fn test_extra_files_deserialize_empty() {
585 let toml_str = r#"
586 [boot]
587 type = "uefi"
588 "#;
589 let config: Config = toml::from_str(toml_str).unwrap();
590 assert!(config.extra_files.is_empty());
591 }
592
593 #[test]
594 fn test_extra_files_deserialize_nested_paths() {
595 let toml_str = r#"
596 [extra-files]
597 "boot/initramfs.cpio" = "build/initramfs.cpio"
598 "boot/data/config.txt" = "data/config.txt"
599 "#;
600 let config: Config = toml::from_str(toml_str).unwrap();
601 assert_eq!(config.extra_files.len(), 2);
602 assert_eq!(
603 config.extra_files.get("boot/initramfs.cpio").unwrap(),
604 "build/initramfs.cpio"
605 );
606 assert_eq!(
607 config.extra_files.get("boot/data/config.txt").unwrap(),
608 "data/config.txt"
609 );
610 }
611}