use super::mount_point::find_mount_point;
use std::{
ffi::OsStr,
fs::canonicalize,
io,
path::{Path, PathBuf},
};
use sysinfo::{Disk, DiskKind};
#[cfg(target_os = "linux")]
use pipe_trait::Pipe;
#[cfg(target_os = "linux")]
use std::borrow::Cow;
pub trait DiskApi {
fn get_disk_kind(&self) -> DiskKind;
fn get_disk_name(&self) -> &OsStr;
fn get_mount_point(&self) -> &Path;
}
pub trait FsApi {
fn canonicalize(path: &Path) -> io::Result<PathBuf>;
#[cfg(target_os = "linux")]
fn path_exists(path: &Path) -> bool;
#[cfg(target_os = "linux")]
fn read_link(path: &Path) -> io::Result<PathBuf>;
}
pub struct RealFs;
impl DiskApi for Disk {
#[inline]
fn get_disk_kind(&self) -> DiskKind {
self.kind()
}
#[inline]
fn get_disk_name(&self) -> &OsStr {
self.name()
}
#[inline]
fn get_mount_point(&self) -> &Path {
self.mount_point()
}
}
impl FsApi for RealFs {
#[inline]
fn canonicalize(path: &Path) -> io::Result<PathBuf> {
canonicalize(path)
}
#[cfg(target_os = "linux")]
#[inline]
fn path_exists(path: &Path) -> bool {
path.exists()
}
#[cfg(target_os = "linux")]
#[inline]
fn read_link(path: &Path) -> io::Result<PathBuf> {
std::fs::read_link(path)
}
}
#[cfg(target_os = "linux")]
const VIRTUAL_DISK_KIND: DiskKind = DiskKind::Unknown(-1);
#[cfg(target_os = "linux")]
fn reclassify_virtual_hdd<Fs: FsApi>(kind: DiskKind, disk_name: &str) -> DiskKind {
if kind != DiskKind::HDD {
return kind;
}
if let Some(block_dev) = extract_block_device_name::<Fs>(disk_name)
&& is_virtual_block_device::<Fs>(&block_dev)
{
return VIRTUAL_DISK_KIND;
}
DiskKind::HDD
}
#[cfg(not(target_os = "linux"))]
fn reclassify_virtual_hdd<Fs: FsApi>(kind: DiskKind, _: &str) -> DiskKind {
kind
}
#[cfg(target_os = "linux")]
fn extract_block_device_name<Fs: FsApi>(device_path: &str) -> Option<Cow<'_, str>> {
if !device_path.starts_with("/dev/mapper/") && !device_path.starts_with("/dev/root") {
let block_dev = parse_block_device_name(device_path)?;
return block_dev
.pipe(validate_block_device::<Fs>)
.map(Cow::Borrowed);
}
let canon_device_path = Fs::canonicalize(Path::new(device_path)).ok()?;
let canon_device_path = canon_device_path.to_str()?;
if canon_device_path == device_path {
return None;
}
canon_device_path
.pipe(extract_block_device_name::<Fs>)
.map(Cow::into_owned) .map(Cow::Owned)
}
#[cfg(target_os = "linux")]
fn parse_block_device_name(device_path: &str) -> Option<&str> {
let name = device_path.strip_prefix("/dev/")?;
let block_dev = if name.starts_with("sd") || name.starts_with("vd") || name.starts_with("xvd") {
name.trim_end_matches(|c: char| c.is_ascii_digit())
} else if name.starts_with("nvme") || name.starts_with("mmcblk") {
match name.rsplit_once('p') {
Some((base, suffix))
if !base.is_empty()
&& !suffix.is_empty()
&& suffix.bytes().all(|b| b.is_ascii_digit()) =>
{
base
}
_ => name,
}
} else {
name
};
Some(block_dev)
}
#[cfg(target_os = "linux")]
fn validate_block_device<Fs: FsApi>(block_dev: &str) -> Option<&str> {
"/sys/block"
.pipe(Path::new)
.join(block_dev)
.pipe_as_ref(Fs::path_exists)
.then_some(block_dev)
}
#[cfg(target_os = "linux")]
fn is_virtual_block_device<Fs: FsApi>(block_dev: &str) -> bool {
let driver_path = "/sys/block"
.pipe(Path::new)
.join(block_dev)
.join("device/driver");
let Ok(target) = Fs::read_link(&driver_path) else {
return false;
};
let driver_name = target.file_name().and_then(OsStr::to_str);
matches!(
driver_name,
Some(
"virtio_blk"
| "virtio-blk"
| "xen_blkfront"
| "xen-blkfront"
| "vbd"
| "vmw_pvscsi"
| "hv_storvsc"
)
)
}
pub fn any_path_is_in_hdd<Disk: DiskApi, Fs: FsApi>(paths: &[PathBuf], disks: &[Disk]) -> bool {
paths
.iter()
.filter_map(|file| Fs::canonicalize(file).ok())
.any(|path| path_is_in_hdd::<Disk, Fs>(&path, disks))
}
fn path_is_in_hdd<Disk: DiskApi, Fs: FsApi>(path: &Path, disks: &[Disk]) -> bool {
let mount_point = find_mount_point(path, disks.iter().map(Disk::get_mount_point));
let Some(mount_point) = mount_point else {
return false;
};
disks
.iter()
.filter(|disk| disk.get_mount_point() == mount_point)
.any(is_hdd::<Fs>)
}
fn is_hdd<Fs: FsApi>(disk: &impl DiskApi) -> bool {
let kind = disk.get_disk_kind();
let name = disk.get_disk_name().to_str();
match name {
Some(name) => reclassify_virtual_hdd::<Fs>(kind, name) == DiskKind::HDD,
None => kind == DiskKind::HDD, }
}
#[cfg(test)]
mod test;
#[cfg(target_os = "linux")]
#[cfg(test)]
mod test_linux;
#[cfg(target_os = "linux")]
#[cfg(test)]
mod test_linux_smoke;