use crate::devices::DeviceError;
use crate::platform::traits::DevicePathOps;
use std::path::Path;
use super::LinuxPlatform;
impl DevicePathOps for LinuxPlatform {
fn validate_whole_disk_path(path: &str) -> Result<(), DeviceError> {
validate_linux_block_device_path(path)
}
fn paths_equivalent(listed: &str, selected: &str) -> bool {
listed == selected
}
}
pub fn validate_linux_block_device_path(path: &str) -> Result<(), DeviceError> {
let trimmed = path.trim();
if trimmed.is_empty() {
return Err(DeviceError::invalid_path("Device path is empty."));
}
if !trimmed.starts_with("/dev/") {
return Err(DeviceError::invalid_path(format!(
"Device must be a full block device path (e.g. /dev/sdb), got: {trimmed}"
)));
}
let name = trimmed
.strip_prefix("/dev/")
.ok_or_else(|| DeviceError::invalid_path(format!("Invalid device path: {trimmed}")))?;
if name.is_empty() {
return Err(DeviceError::invalid_path(format!("Invalid device path: {trimmed}")));
}
if is_partition_block_name(name) {
return Err(DeviceError::invalid_path(format!(
"Partitions are not allowed; use the whole block device (got {trimmed})"
)));
}
if is_rejected_block_name(name) {
return Err(DeviceError::invalid_path(format!(
"Device type is not allowed for flash/clone: {trimmed}"
)));
}
if !is_whole_block_device_name(name) {
return Err(DeviceError::invalid_path(format!(
"Not a recognized whole block device: {trimmed}"
)));
}
if !Path::new(trimmed).exists() {
return Err(DeviceError::invalid_path(format!(
"Device path does not exist: {trimmed}"
)));
}
Ok(())
}
pub fn whole_disk_path(path_or_name: &str) -> Result<String, DeviceError> {
let name = if path_or_name.starts_with("/dev/") {
path_or_name
.strip_prefix("/dev/")
.ok_or_else(|| {
DeviceError::invalid_path(format!("Invalid device path: {path_or_name}"))
})?
} else {
path_or_name
};
Ok(format!("/dev/{}", whole_disk_name_from_block(name)))
}
pub(crate) fn whole_disk_path_from_source(source: &str) -> Result<String, DeviceError> {
if !source.starts_with("/dev/") {
return Err(DeviceError::query(format!(
"Unsupported root device source: {source}"
)));
}
whole_disk_path(source)
}
fn scsi_disk_stem(name: &str) -> Option<&str> {
name.strip_prefix("sd")
.or_else(|| name.strip_prefix("vd"))
.or_else(|| name.strip_prefix("hd"))
}
pub(crate) fn is_partition_block_name(name: &str) -> bool {
if name.starts_with("mmcblk") {
return name.rsplit_once('p').is_some_and(|(_, suffix)| {
!suffix.is_empty() && suffix.chars().all(|c| c.is_ascii_digit())
});
}
if name.starts_with("nvme") {
return name.rsplit_once('p').is_some_and(|(prefix, suffix)| {
prefix.contains('n') && !suffix.is_empty() && suffix.chars().all(|c| c.is_ascii_digit())
});
}
if let Some(stem) = scsi_disk_stem(name) {
return stem.chars().any(|c| c.is_ascii_digit());
}
false
}
fn is_rejected_block_name(name: &str) -> bool {
name.starts_with("loop") || name.starts_with("dm-") || name.starts_with("ram")
}
fn whole_disk_name_from_block(name: &str) -> String {
if is_partition_block_name(name) {
if let Some((stem, suffix)) = name.rsplit_once('p') {
if (name.starts_with("mmcblk") || name.starts_with("nvme"))
&& !suffix.is_empty()
&& suffix.chars().all(|c| c.is_ascii_digit())
{
return stem.to_string();
}
}
let prefix: String = name.chars().take_while(|c| !c.is_ascii_digit()).collect();
if !prefix.is_empty() && prefix.len() < name.len() {
return prefix;
}
}
name.to_string()
}
pub(crate) fn is_whole_block_device_name(name: &str) -> bool {
if is_partition_block_name(name) || is_rejected_block_name(name) {
return false;
}
if let Some(stem) = scsi_disk_stem(name) {
return !stem.is_empty()
&& stem.chars().all(|c| c.is_ascii_lowercase())
&& !stem.chars().any(|c| c.is_ascii_digit());
}
if let Some(rest) = name.strip_prefix("mmcblk") {
return rest.chars().all(|c| c.is_ascii_digit());
}
if let Some(rest) = name.strip_prefix("nvme") {
return !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit() || c == 'n');
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_whole_block_paths() {
for name in ["sdb", "mmcblk0", "nvme0n1", "vda"] {
assert!(is_whole_block_device_name(name), "{name}");
assert!(!is_partition_block_name(name), "{name}");
}
}
#[test]
fn rejects_partitions_and_loops() {
for name in ["sdb1", "mmcblk0p1", "nvme0n1p2", "loop0", "dm-0"] {
assert!(
is_partition_block_name(name) || is_rejected_block_name(name),
"{name}"
);
}
}
#[test]
fn validate_requires_dev_prefix() {
assert!(validate_linux_block_device_path("sdb").is_err());
assert!(validate_linux_block_device_path("/dev/sdb1").is_err());
}
#[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");
}
}