use crate::platform::traits::{
DeviceInventory, DevicePathOps, DeviceSafety, Platform, VolumeOps,
};
use crate::platform::Active;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::fmt;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum DeviceError {
#[error("{0}")]
InvalidPath(String),
#[error("{0}")]
SystemDisk(String),
#[error("{0}")]
Busy(String),
#[error("{0}")]
UnmountFailed(String),
#[error("{0}")]
NotListed(String),
#[error("{0}")]
Unsupported(String),
#[error("{0}")]
QueryFailed(String),
}
impl DeviceError {
pub fn invalid_path(msg: impl Into<String>) -> Self {
Self::InvalidPath(msg.into())
}
pub fn system_disk(msg: impl Into<String>) -> Self {
Self::SystemDisk(msg.into())
}
pub fn busy(msg: impl Into<String>) -> Self {
Self::Busy(msg.into())
}
pub fn unmount(msg: impl Into<String>) -> Self {
Self::UnmountFailed(msg.into())
}
pub fn not_listed(msg: impl Into<String>) -> Self {
Self::NotListed(msg.into())
}
pub fn unsupported(msg: impl Into<String>) -> Self {
Self::Unsupported(msg.into())
}
pub fn query(msg: impl Into<String>) -> Self {
Self::QueryFailed(msg.into())
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DeviceInfo {
pub device_name: String,
pub vendor_name: String,
pub model_name: String,
pub removable: u8,
pub size: u64,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct DeviceMount {
pub source: String,
pub mount_point: String,
}
impl fmt::Display for DeviceInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", json!(self))
}
}
pub fn validate_block_device_path(path: &str) -> Result<(), DeviceError> {
Active::validate_whole_disk_path(path)
}
pub fn validate_listed_block_device(
path: &str,
known: &[impl AsRef<str>],
) -> Result<(), DeviceError> {
validate_device_for_io(path)?;
let listed = known
.iter()
.any(|entry| Active::paths_equivalent(entry.as_ref(), path));
if !listed {
return Err(DeviceError::not_listed(format!(
"Device {path} is not in the current device list. Refresh devices and select again."
)));
}
Ok(())
}
pub fn validate_device_for_io(path: &str) -> Result<(), DeviceError> {
Active::validate_for_io(path)
}
pub fn validate_device_safe_for_io(path: &str) -> Result<(), DeviceError> {
Active::validate_safe_for_io(path)
}
pub fn list_mounted_drive_letters(path: &str) -> Result<Vec<String>, DeviceError> {
Ok(Active::list_mounts(path)?
.into_iter()
.map(|m| m.source)
.collect())
}
pub fn validate_device_not_system_disk(path: &str) -> Result<(), DeviceError> {
Active::refuse_system_disk(path)
}
pub fn list_device_mounts(device_path: &str) -> Result<Vec<DeviceMount>, DeviceError> {
Active::list_mounts(device_path)
}
pub fn format_device_busy_error(device_path: &str, mounts: &[DeviceMount]) -> String {
if mounts.is_empty() {
return format!("{device_path} is not available for raw disk I/O.");
}
let details: Vec<String> = mounts
.iter()
.map(|m| format!("{} on {}", m.source, m.mount_point))
.collect();
let hint = Active::busy_hint();
format!(
"Cannot use {device_path}: {}. {}",
if details.len() == 1 {
format!("{} is still mounted", details[0])
} else {
format!("these volumes are still mounted: {}", details.join("; "))
},
hint
)
}
pub fn unmount_device_volumes(device_path: &str) -> Result<Vec<String>, DeviceError> {
Active::unmount_volumes(device_path)
}
pub fn ensure_device_ready_for_io(device_path: &str, auto_unmount: bool) -> Result<(), DeviceError> {
Active::preflight_for_io(device_path, auto_unmount)
}
pub fn validate_device_not_busy(path: &str) -> Result<(), DeviceError> {
Active::refuse_if_busy(path)
}
pub fn whole_disk_path(path_or_name: &str) -> Result<String, DeviceError> {
#[cfg(target_os = "linux")]
{
crate::platform::linux::whole_disk_path(path_or_name)
}
#[cfg(not(target_os = "linux"))]
{
Active::validate_whole_disk_path(path_or_name)?;
Ok(path_or_name.trim().to_string())
}
}
pub fn device_paths_equivalent(listed: &str, selected: &str) -> bool {
Active::paths_equivalent(listed, selected)
}
pub fn hint_for_io_error(err: &std::io::Error) -> Option<String> {
Active::hint_for_io_error(err)
}
const IO_BLOCK_SIZES: [usize; 14] = [
4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, 2097152, 4194304, 8388608,
16777216, 33554432,
];
const DEFAULT_IO_BLOCK_SIZE: usize = IO_BLOCK_SIZES[0];
pub const REMOVABLE_IO_BLOCK_CAP: usize = 4_194_304;
pub fn optimal_io_block_size_from_sectors(size_sectors: u64) -> usize {
if size_sectors > IO_BLOCK_SIZES[13] as u64 {
return IO_BLOCK_SIZES[13];
}
if size_sectors < IO_BLOCK_SIZES[0] as u64 {
return IO_BLOCK_SIZES[0];
}
IO_BLOCK_SIZES
.iter()
.copied()
.find(|&block| size_sectors <= block as u64)
.unwrap_or(IO_BLOCK_SIZES[13])
}
pub fn optimal_io_block_size_for_media(size_sectors: u64, removable: bool) -> usize {
let selected = optimal_io_block_size_from_sectors(size_sectors);
if removable {
selected.min(REMOVABLE_IO_BLOCK_CAP)
} else {
selected
}
}
pub fn optimal_io_block_size(device_path: &str) -> usize {
let removable = is_removable_device(device_path).unwrap_or(true);
device_size_sectors(device_path)
.map(|sectors| optimal_io_block_size_for_media(sectors, removable))
.unwrap_or(DEFAULT_IO_BLOCK_SIZE)
}
pub fn device_size_sectors(device_path: &str) -> Option<u64> {
Active::device_size_sectors(device_path)
}
pub fn device_size_bytes(device_path: &str) -> Option<u64> {
Active::device_size_bytes(device_path)
}
pub fn is_removable_device(device_path: &str) -> Result<bool> {
Active::is_removable(device_path)
}
pub fn get_storage_devices() -> Result<Vec<DeviceInfo>> {
Active::list_storage_devices()
}
#[cfg(test)]
mod validation_tests {
use super::*;
#[test]
fn validate_requires_dev_prefix() {
assert!(validate_block_device_path("sdb").is_err());
}
#[cfg(target_os = "windows")]
#[test]
fn validate_rejects_non_physical_drive_paths() {
assert!(validate_block_device_path(r"\\.\C:").is_err());
assert!(validate_block_device_path("sdb").is_err());
}
#[test]
fn optimal_io_block_size_from_sectors_matches_lithographer_table() {
assert_eq!(optimal_io_block_size_from_sectors(2_048), 4_096);
assert_eq!(optimal_io_block_size_from_sectors(4_096), 4_096);
assert_eq!(optimal_io_block_size_from_sectors(5_000), 8_192);
assert_eq!(optimal_io_block_size_from_sectors(100_000), 131_072);
assert_eq!(optimal_io_block_size_from_sectors(2_097_152), 2_097_152);
}
#[test]
fn optimal_io_block_size_from_sectors_clamps_large_disks() {
assert_eq!(optimal_io_block_size_from_sectors(100_000_000), 33_554_432);
assert_eq!(optimal_io_block_size_from_sectors(500), 4_096);
}
#[test]
fn optimal_io_block_size_for_media_caps_removable() {
assert_eq!(
optimal_io_block_size_for_media(100_000_000, true),
REMOVABLE_IO_BLOCK_CAP
);
assert_eq!(
optimal_io_block_size_for_media(2_097_152, true),
2_097_152
);
assert_eq!(
optimal_io_block_size_for_media(100_000_000, false),
33_554_432
);
}
#[test]
fn format_device_busy_error_is_actionable() {
let mounts = vec![DeviceMount {
source: "/dev/sda1".to_string(),
mount_point: "/boot".to_string(),
}];
let msg = format_device_busy_error("/dev/sda", &mounts);
assert!(msg.contains("/dev/sda"));
assert!(msg.contains("/dev/sda1"));
assert!(msg.contains("/boot"));
assert!(msg.contains("unmount") || msg.contains("Confirm") || msg.contains("Unmount"));
}
#[cfg(target_os = "linux")]
#[test]
fn whole_disk_path_strips_partitions() {
assert_eq!(whole_disk_path("/dev/sdb1").unwrap(), "/dev/sdb");
assert_eq!(whole_disk_path("/dev/nvme0n1p2").unwrap(), "/dev/nvme0n1");
assert_eq!(whole_disk_path("/dev/mmcblk0p1").unwrap(), "/dev/mmcblk0");
assert_eq!(whole_disk_path("/dev/sdb").unwrap(), "/dev/sdb");
}
}