Skip to main content

bootsmith_disk/
lib.rs

1//! Raw device access. The single chokepoint through which bootsmith touches
2//! `/dev/rdiskN`. Lives behind the `Device` trait from `bootsmith-core`.
3//!
4//! Safety guards (the most important code in the project, after the boot
5//! records themselves):
6//!
7//! - Refuse the boot disk.
8//! - Refuse any disk flagged `internal: true` by DiskArbitration.
9//! - Refuse disks larger than 256 GiB without `--force`.
10//! - Always operate on `/dev/rdiskN`, never `/dev/diskN`.
11//!
12//! Implementations are gated by `cfg(target_os = ...)`. macOS is the only
13//! target in v1; Linux is planned for v2.
14
15use thiserror::Error;
16
17#[cfg(target_os = "macos")]
18pub mod macos;
19
20#[cfg(target_os = "macos")]
21pub mod raw;
22
23#[derive(Debug, Error)]
24pub enum DiskError {
25    // No `{0}` in Display: anyhow's `{:#}` chain printer walks
26    // .source() and would otherwise print the io::Error twice.
27    #[error("disk I/O")]
28    Io(#[from] std::io::Error),
29
30    #[error("refusing to write to {0}: this looks like the boot disk")]
31    RefusedBootDisk(String),
32
33    #[error("refusing to write to {0}: marked as internal storage")]
34    RefusedInternal(String),
35
36    #[error(
37        "refusing to write to {device} ({size_gb} GB): exceeds 256 GiB safety threshold. \
38         Pass --force if you really mean it."
39    )]
40    RefusedTooLarge { device: String, size_gb: u64 },
41
42    #[error("device path must be /dev/rdiskN, got: {0}")]
43    BadDevicePath(String),
44
45    #[error("DiskArbitration query failed: {0}")]
46    DaError(String),
47
48    #[error("external command failed: {cmd}: {stderr}")]
49    External { cmd: String, stderr: String },
50}
51
52pub type Result<T> = std::result::Result<T, DiskError>;
53
54/// Caller-provided safety overrides. `Default` is the safe configuration.
55#[derive(Debug, Clone, Default)]
56pub struct SafetyConfig {
57    /// Skip the 256 GiB cap and the internal-disk check. CLI flag: `--force`.
58    pub force: bool,
59}
60
61/// Description of a candidate target device, returned by enumeration so we
62/// can show the user a clear confirm prompt.
63#[derive(Debug, Clone)]
64pub struct DeviceInfo {
65    pub path: String,         // e.g. "/dev/rdisk8"
66    pub size_bytes: u64,
67    pub model: String,        // e.g. "SanDisk Cruzer Blade"
68    pub internal: bool,
69    pub is_boot_disk: bool,
70    pub removable: bool,
71}
72
73impl DeviceInfo {
74    /// Apply the safety policy. Returns Ok(()) if the device may be written.
75    pub fn check_writable(&self, safety: &SafetyConfig) -> Result<()> {
76        if self.is_boot_disk {
77            return Err(DiskError::RefusedBootDisk(self.path.clone()));
78        }
79        if self.internal && !safety.force {
80            return Err(DiskError::RefusedInternal(self.path.clone()));
81        }
82        let cap = 256u64 * 1024 * 1024 * 1024;
83        if self.size_bytes > cap && !safety.force {
84            return Err(DiskError::RefusedTooLarge {
85                device: self.path.clone(),
86                size_gb: self.size_bytes / 1_000_000_000,
87            });
88        }
89        Ok(())
90    }
91}