use crate::devices::{DeviceError, DeviceMount};
use crate::platform::traits::DeviceSafety;
use std::fs;
use std::path::Path;
use std::process::Command;
use super::path::{whole_disk_path, whole_disk_path_from_source};
use super::LinuxPlatform;
impl DeviceSafety for LinuxPlatform {
fn refuse_system_disk(path: &str) -> Result<(), DeviceError> {
let target_whole = whole_disk_path(path)?;
let root_sources = root_filesystem_sources()?;
for source in &root_sources {
let source_whole = whole_disk_path_from_source(source)?;
if source_whole == target_whole {
return Err(DeviceError::system_disk(format!(
"Refusing {path}: it is the system disk (root filesystem is on {source})"
)));
}
}
Ok(())
}
fn list_mounts(device_path: &str) -> Result<Vec<DeviceMount>, DeviceError> {
busy_mounts_for_device(device_path).map(|mounts| {
mounts
.into_iter()
.map(|(source, mount_point)| DeviceMount {
source,
mount_point,
})
.collect()
})
}
fn busy_hint() -> &'static str {
"Confirm the operation to unmount volumes automatically, or run `sudo umount <partition>` first."
}
}
fn normalize_mount_source(source: &str) -> String {
source
.split_once('[')
.map(|(base, _)| base)
.unwrap_or(source)
.trim()
.to_string()
}
fn root_filesystem_sources() -> Result<Vec<String>, DeviceError> {
match root_filesystem_source_from_findmnt() {
Ok(source) => expand_block_sources(&normalize_mount_source(&source)),
Err(findmnt_err) => {
let source = root_filesystem_source_from_proc_mounts()
.map_err(|proc_err| DeviceError::query(format!("{findmnt_err}; fallback: {proc_err}")))?;
expand_block_sources(&normalize_mount_source(&source))
}
}
}
fn root_filesystem_source_from_findmnt() -> Result<String, DeviceError> {
let output = Command::new("findmnt")
.args(["-n", "-o", "SOURCE", "--target", "/"])
.output()
.map_err(|e| DeviceError::query(format!("Failed to run findmnt: {e}")))?;
if !output.status.success() {
return Err(DeviceError::query(format!(
"findmnt failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
)));
}
let source = String::from_utf8_lossy(&output.stdout).trim().to_string();
if source.is_empty() {
return Err(DeviceError::invalid_path(
"findmnt returned an empty root source",
));
}
Ok(source)
}
fn root_filesystem_source_from_proc_mounts() -> Result<String, DeviceError> {
let contents = fs::read_to_string("/proc/mounts")
.map_err(|e| DeviceError::query(format!("Failed to read /proc/mounts: {e}")))?;
for line in contents.lines() {
let mut parts = line.split_whitespace();
let Some(source) = parts.next() else {
continue;
};
let Some(mount_point) = parts.next() else {
continue;
};
if mount_point == "/" {
return Ok(source.to_string());
}
}
Err(DeviceError::query("Root mount not found in /proc/mounts"))
}
fn expand_block_sources(source: &str) -> Result<Vec<String>, DeviceError> {
if !source.starts_with("/dev/") {
return Err(DeviceError::query(format!(
"Root filesystem source {source} is not a block device path"
)));
}
let block_name = Path::new(source)
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| DeviceError::invalid_path(format!("Invalid root device source: {source}")))?;
if block_name.starts_with("dm-") || source.contains("/mapper/") {
if let Some(dm_name) = dm_sysfs_name(source).or_else(|| {
fs::read_link(source)
.ok()
.and_then(|link| {
link.file_name()
.and_then(|n| n.to_str())
.map(str::to_string)
})
.filter(|name| name.starts_with("dm-"))
}) {
let slaves_dir = format!("/sys/block/{dm_name}/slaves");
if let Ok(entries) = fs::read_dir(&slaves_dir) {
let slaves: Vec<String> = entries
.filter_map(|entry| entry.ok())
.filter_map(|entry| entry.file_name().into_string().ok())
.map(|name| format!("/dev/{name}"))
.collect();
if !slaves.is_empty() {
return Ok(slaves);
}
}
}
}
Ok(vec![source.to_string()])
}
fn dm_sysfs_name(source: &str) -> Option<String> {
let path = Path::new(source);
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if name.starts_with("dm-") {
return Some(name.to_string());
}
}
let link = fs::read_link(source).ok()?;
let link_name = link.file_name().and_then(|n| n.to_str())?;
if link_name.starts_with("dm-") {
return Some(link_name.to_string());
}
None
}
fn busy_mounts_for_device(device_path: &str) -> Result<Vec<(String, String)>, DeviceError> {
let target_whole = whole_disk_path(device_path)?;
let contents = fs::read_to_string("/proc/mounts")
.map_err(|e| DeviceError::query(format!("Failed to read /proc/mounts: {e}")))?;
let mut mounts = Vec::new();
for line in contents.lines() {
let mut parts = line.split_whitespace();
let Some(source) = parts.next() else {
continue;
};
let Some(mount_point) = parts.next() else {
continue;
};
if !source.starts_with("/dev/") {
continue;
}
let source_whole = whole_disk_path_from_source(source)?;
if source_whole == target_whole {
mounts.push((source.to_string(), mount_point.to_string()));
}
}
Ok(mounts)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::platform::linux::path::whole_disk_path;
fn busy_mounts_from_lines(
contents: &str,
device_path: &str,
) -> Result<Vec<(String, String)>, DeviceError> {
let target_whole = whole_disk_path(device_path)?;
let mut mounts = Vec::new();
for line in contents.lines() {
let mut parts = line.split_whitespace();
let Some(source) = parts.next() else {
continue;
};
let Some(mount_point) = parts.next() else {
continue;
};
if !source.starts_with("/dev/") {
continue;
}
let source_whole = whole_disk_path_from_source(source)?;
if source_whole == target_whole {
mounts.push((source.to_string(), mount_point.to_string()));
}
}
Ok(mounts)
}
#[test]
fn busy_mounts_detects_partition_on_target_disk() {
let proc_mounts = r#"
/dev/nvme0n1p2 / ext4 rw,relatime 0 0
/dev/sdb1 /mnt/usb vfat rw,relatime 0 0
/dev/sdb2 /media/backup ext4 rw,relatime 0 0
"#;
let mounts = busy_mounts_from_lines(proc_mounts, "/dev/sdb").unwrap();
assert_eq!(mounts.len(), 2);
assert!(mounts
.iter()
.any(|(s, m)| s == "/dev/sdb1" && m == "/mnt/usb"));
assert!(mounts
.iter()
.any(|(s, m)| s == "/dev/sdb2" && m == "/media/backup"));
}
#[test]
fn busy_mounts_ignores_other_disks() {
let proc_mounts = "/dev/nvme0n1p2 / ext4 rw,relatime 0 0\n";
let mounts = busy_mounts_from_lines(proc_mounts, "/dev/sdb").unwrap();
assert!(mounts.is_empty());
}
}