Skip to main content

bootsmith_core/
lib.rs

1//! bootsmith-core: the typed pipeline that turns an ISO + a target device into a
2//! sequence of write operations.
3//!
4//! This crate is intentionally OS-agnostic. Concrete device I/O lives in
5//! `bootsmith-disk`; ISO inspection in `bootsmith-iso`; boot-record bytes in
6//! `bootsmith-boot`. The orchestration here calls into those via traits so the
7//! pipeline can run against an in-memory `Vec<u8>` for unit tests just as
8//! easily as against `/dev/rdisk8`.
9
10use std::fmt;
11use std::path::PathBuf;
12use thiserror::Error;
13
14pub mod device;
15pub mod plan;
16
17pub use device::Device;
18pub use plan::{BootMode, WritePlan};
19
20#[derive(Debug, Error)]
21pub enum Error {
22    // No `{0}` in Display: anyhow's `{:#}` chain printer walks
23    // .source() and would otherwise print the io::Error twice.
24    #[error("I/O error")]
25    Io(#[from] std::io::Error),
26
27    #[error("device refuses to be written: {0}")]
28    DeviceRefused(String),
29
30    #[error("ISO does not fit on device (iso={iso_bytes} bytes, device={device_bytes} bytes)")]
31    IsoTooLarge { iso_bytes: u64, device_bytes: u64 },
32
33    #[error("ISO classification failed: {0}")]
34    IsoClassify(String),
35
36    #[error("boot record write failed: {0}")]
37    BootRecord(String),
38
39    #[error("verification failed at offset {offset}: expected {expected:02x?}, got {actual:02x?}")]
40    VerifyMismatch {
41        offset: u64,
42        expected: Vec<u8>,
43        actual: Vec<u8>,
44    },
45
46    #[error("unsupported boot mode for this ISO: {0}")]
47    UnsupportedMode(String),
48
49    #[error("external command failed: {cmd}: {stderr}")]
50    External { cmd: String, stderr: String },
51}
52
53pub type Result<T> = std::result::Result<T, Error>;
54
55/// Top-level config built from CLI args.
56#[derive(Debug, Clone)]
57pub struct Config {
58    pub iso_path: PathBuf,
59    pub device_path: PathBuf,
60    pub mode: ModeRequest,
61    pub label: Option<String>,
62    pub dry_run: bool,
63    pub force: bool,
64    pub verify: bool,
65    pub verbose: bool,
66    /// Which implementation writes the MBR boot code and partition boot
67    /// record. `Bootrec` links the native Rust library in-process;
68    /// `MsSys` shells out to the upstream tool. See
69    /// docs/V1_BOOTREC_LIBRARY.md.
70    pub boot_record_impl: BootRecordImpl,
71    /// Optional NT5 answer-file settings. Currently consumed only by the
72    /// Windows 2000/XP GRUB4DOS + FiraDisk path.
73    pub unattended: Option<UnattendedConfig>,
74    /// Optional path to a vendor-shaped F6 driver folder (e.g. Intel iaStor)
75    /// to be merged with the FiraDisk floppy for XP textmode AHCI support.
76    /// User-supplied; bootsmith does not bundle any third-party storage driver.
77    /// Expects `txtsetup.oem` plus the `.sys`/`.inf`/`.cat` files it lists.
78    pub ahci_driver_dir: Option<PathBuf>,
79}
80
81#[derive(Clone)]
82pub struct UnattendedConfig {
83    pub product_key: Option<String>,
84    pub full_name: String,
85    pub organization: String,
86    pub computer_name: String,
87    pub admin_password: Option<String>,
88    pub timezone: Option<u16>,
89}
90
91impl fmt::Debug for UnattendedConfig {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        f.debug_struct("UnattendedConfig")
94            .field(
95                "product_key",
96                &self.product_key.as_ref().map(|_| "<redacted>"),
97            )
98            .field("full_name", &self.full_name)
99            .field("organization", &self.organization)
100            .field("computer_name", &self.computer_name)
101            .field(
102                "admin_password",
103                &self.admin_password.as_ref().map(|_| "<redacted>"),
104            )
105            .field("timezone", &self.timezone)
106            .finish()
107    }
108}
109
110/// Backend used to write MBR boot code and the partition boot record.
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum BootRecordImpl {
113    /// Shell out to the external `ms-sys` binary. Legacy v0.2 path.
114    MsSys,
115    /// Use the in-process `bootrec` library. Default from v1.0.
116    Bootrec,
117}
118
119impl BootRecordImpl {
120    pub fn as_str(&self) -> &'static str {
121        match self {
122            BootRecordImpl::MsSys => "ms-sys",
123            BootRecordImpl::Bootrec => "bootrec",
124        }
125    }
126}
127
128/// What the user asked for at the CLI. `Auto` triggers ISO inspection to
129/// pick the actual `BootMode`.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum ModeRequest {
132    Auto,
133    Windows,
134    WindowsNtXp,
135    Windows2000,
136    IsolinuxLinux,
137    Hybrid,
138    UefiOnly,
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn unattended_debug_redacts_secrets() {
147        let config = UnattendedConfig {
148            product_key: Some("AAAAA-BBBBB-CCCCC-DDDDD-EEEEE".into()),
149            full_name: "QA User".into(),
150            organization: "bootsmith".into(),
151            computer_name: "XPTEST".into(),
152            admin_password: Some("secret".into()),
153            timezone: Some(35),
154        };
155
156        let debug = format!("{config:?}");
157        assert!(debug.contains("<redacted>"));
158        assert!(!debug.contains("AAAAA-BBBBB"));
159        assert!(!debug.contains("secret"));
160    }
161}