use std::{
ffi::CString,
fs::File,
io::{self, BufRead, BufReader},
mem,
path::{Path, PathBuf},
str::FromStr,
};
use anyhow::bail;
use crate::collection::disks::unix::{FileSystem, Usage};
pub(crate) struct Partition {
device: Option<String>,
mount_point: PathBuf,
fs_type: FileSystem,
}
impl Partition {
#[inline]
pub fn device(&self) -> Option<&str> {
self.device.as_deref()
}
#[inline]
pub fn mount_point(&self) -> &Path {
self.mount_point.as_path()
}
#[inline]
pub fn fs_type(&self) -> &FileSystem {
&self.fs_type
}
pub fn get_device_name(&self) -> String {
if let Some(device) = self.device() {
if let Ok(path) = std::fs::read_link(device) {
if path.is_absolute() {
path.into_os_string()
.into_string()
.unwrap_or_else(|_| "Name Unavailable".to_string())
} else {
let mut combined_path = PathBuf::new();
combined_path.push(device);
combined_path.pop(); combined_path.push(path);
if let Ok(canon_path) = std::fs::canonicalize(combined_path) {
canon_path
.into_os_string()
.into_string()
.unwrap_or_else(|_| "Name Unavailable".to_string())
} else {
device.to_owned()
}
}
} else {
device.to_owned()
}
} else {
"Name Unavailable".to_string()
}
}
pub fn usage(&self) -> anyhow::Result<Usage> {
let path = self
.mount_point
.to_str()
.ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))
.and_then(|string| {
CString::new(string).map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))
})
.map_err(|e| anyhow::anyhow!("invalid path: {e:?}"))?;
let mut vfs = mem::MaybeUninit::<libc::statvfs>::uninit();
let result = unsafe { libc::statvfs(path.as_ptr(), vfs.as_mut_ptr()) };
if result == 0 {
let vfs = unsafe { vfs.assume_init() };
Ok(Usage::new(vfs))
} else {
Err(anyhow::anyhow!(
"statvfs had an issue getting info from {path:?}"
))
}
}
}
fn fix_mount_point(s: &str) -> String {
const ESCAPED_BACKSLASH: &str = "\\134";
const ESCAPED_SPACE: &str = "\\040";
const ESCAPED_TAB: &str = "\\011";
const ESCAPED_NEWLINE: &str = "\\012";
s.replace(ESCAPED_BACKSLASH, "\\")
.replace(ESCAPED_SPACE, " ")
.replace(ESCAPED_TAB, "\t")
.replace(ESCAPED_NEWLINE, "\n")
}
impl FromStr for Partition {
type Err = anyhow::Error;
fn from_str(line: &str) -> anyhow::Result<Partition> {
let mut parts = line.trim_start().splitn(5, ' ');
let device = match parts.next() {
Some("none") => None,
Some(device) => Some(device.to_string()),
None => {
bail!("missing device");
}
};
let mount_point = match parts.next() {
Some(mount_point) => PathBuf::from(fix_mount_point(mount_point)),
None => {
bail!("missing mount point");
}
};
let fs_type = match parts.next() {
Some(fs) => FileSystem::from_str(fs)?,
_ => {
bail!("missing filesystem type");
}
};
Ok(Partition {
device,
mount_point,
fs_type,
})
}
}
#[expect(dead_code)]
pub(crate) fn partitions() -> anyhow::Result<Vec<Partition>> {
const PROC_MOUNTS: &str = "/proc/mounts";
let mut results = vec![];
let mut reader = BufReader::new(File::open(PROC_MOUNTS)?);
let mut line = String::new();
while let Ok(bytes) = reader.read_line(&mut line) {
if bytes > 0 {
if let Ok(partition) = Partition::from_str(&line) {
results.push(partition);
}
line.clear();
} else {
break;
}
}
Ok(results)
}
pub(crate) fn physical_partitions() -> anyhow::Result<Vec<Partition>> {
const PROC_MOUNTS: &str = "/proc/mounts";
let mut results = vec![];
let mut reader = BufReader::new(File::open(PROC_MOUNTS)?);
let mut line = String::new();
while let Ok(bytes) = reader.read_line(&mut line) {
if bytes > 0 {
if let Ok(partition) = Partition::from_str(&line) {
if partition.fs_type().is_physical() {
results.push(partition);
}
}
line.clear();
} else {
break;
}
}
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fix_mount_point() {
let line = "/run/media/test/Samsung\\040980";
assert_eq!(fix_mount_point(line), "/run/media/test/Samsung 980");
}
}