use crate::PathError;
use crate::platform::{DiskInfo, FileAttributes, PathExt, PlatformPath};
use std::fs;
use std::path::{Path, PathBuf};
pub struct UnixPathExt {
path: PathBuf,
}
impl UnixPathExt {
pub fn new<P: AsRef<Path>>(path: P) -> Self {
Self {
path: path.as_ref().to_path_buf(),
}
}
}
impl PlatformPath for UnixPathExt {
fn separator(&self) -> char {
'/'
}
fn is_absolute(&self) -> bool {
self.path.is_absolute()
}
fn to_platform_specific(&self) -> String {
self.path.to_string_lossy().into_owned()
}
}
impl PathExt for UnixPathExt {
fn get_attributes(&self) -> Option<FileAttributes> {
let metadata = fs::metadata(&self.path).ok()?;
let size = metadata.len();
let is_directory = metadata.is_dir();
let is_readonly = metadata.permissions().readonly();
let is_hidden = self
.path
.file_name()
.and_then(|n| n.to_str())
.is_some_and(|s| s.starts_with('.'));
let creation_time = metadata
.created()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs());
let modification_time = metadata
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs());
Some(FileAttributes {
size,
is_directory,
is_hidden,
is_readonly,
creation_time,
modification_time,
})
}
fn is_accessible(&self) -> bool {
self.path.exists()
}
fn get_disk_info(&self) -> Option<DiskInfo> {
let stats = get_filesystem_stats(&self.path).ok()?;
Some(DiskInfo {
total_space: stats.total_blocks.saturating_mul(stats.block_size),
free_space: stats.available_blocks.saturating_mul(stats.block_size),
filesystem_type: "Unix".to_string(),
})
}
}
#[must_use]
pub fn is_absolute_unix_path(path: &str) -> bool {
path.starts_with('/')
}
#[must_use]
pub fn parse_unix_mount_point(path: &str) -> Option<(&str, &str)> {
if let Some(stripped) = path.strip_prefix("/mnt/")
&& let Some(pos) = stripped.find('/')
{
let drive = &stripped[..pos];
let rest = &stripped[pos..];
return Some((drive, rest));
}
if let Some(stripped) = path.strip_prefix('/')
&& let Some(pos) = stripped.find('/')
{
let first_component = &stripped[..pos];
if first_component.len() == 1
&& first_component.chars().next().unwrap().is_ascii_lowercase()
{
return Some((first_component, &stripped[pos..]));
}
}
None
}
pub fn get_unix_path_stats(path: &Path) -> Result<PathStats, PathError> {
let metadata = fs::metadata(path)?;
Ok(PathStats {
size: metadata.len(),
is_dir: metadata.is_dir(),
permissions: metadata.permissions(),
modified: metadata.modified().ok(),
accessed: metadata.accessed().ok(),
created: metadata.created().ok(),
})
}
#[derive(Debug, Clone)]
pub struct PathStats {
pub size: u64,
pub is_dir: bool,
pub permissions: fs::Permissions,
pub modified: Option<std::time::SystemTime>,
pub accessed: Option<std::time::SystemTime>,
pub created: Option<std::time::SystemTime>,
}
#[must_use]
pub fn is_standard_unix_directory(path: &str) -> bool {
let standard_dirs = vec![
"/bin", "/boot", "/dev", "/etc", "/home", "/lib", "/lib64", "/media", "/mnt", "/opt",
"/proc", "/root", "/run", "/sbin", "/srv", "/sys", "/tmp", "/usr", "/var",
];
standard_dirs.iter().any(|&dir| path.starts_with(dir))
}
pub fn get_filesystem_stats(path: &Path) -> Result<FilesystemStats, PathError> {
let path_cstr = std::ffi::CString::new(path.to_string_lossy().as_ref())
.map_err(|e| PathError::platform_error(e.to_string()))?;
let mut statfs: libc::statvfs = unsafe { std::mem::zeroed() };
unsafe {
if libc::statvfs(path_cstr.as_ptr(), &raw mut statfs) != 0 {
return Err(PathError::platform_error(format!(
"Failed to get filesystem stats for {}",
path.display()
)));
}
}
#[allow(clippy::unnecessary_cast)]
{
Ok(FilesystemStats {
block_size: statfs.f_bsize as u64,
total_blocks: statfs.f_blocks as u64,
free_blocks: statfs.f_bfree as u64,
available_blocks: statfs.f_bavail as u64,
total_inodes: statfs.f_files as u64,
free_inodes: statfs.f_ffree as u64,
filesystem_id: statfs.f_fsid as u64,
mount_flags: statfs.f_flag as u64,
max_filename_length: statfs.f_namemax as u64,
})
}
}
#[derive(Debug, Clone)]
pub struct FilesystemStats {
pub block_size: u64,
pub total_blocks: u64,
pub free_blocks: u64,
pub available_blocks: u64,
pub total_inodes: u64,
pub free_inodes: u64,
pub filesystem_id: u64,
pub mount_flags: u64,
pub max_filename_length: u64,
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use tempfile::TempDir;
#[test]
fn test_is_absolute_unix_path() {
assert!(is_absolute_unix_path("/home/user"));
assert!(is_absolute_unix_path("/"));
assert!(!is_absolute_unix_path("relative/path"));
assert!(!is_absolute_unix_path("./relative"));
}
#[test]
fn test_parse_unix_mount_point() {
assert_eq!(
parse_unix_mount_point("/mnt/c/Users"),
Some(("c", "/Users"))
);
assert_eq!(parse_unix_mount_point("/c/Users"), Some(("c", "/Users")));
assert_eq!(parse_unix_mount_point("/home/user"), None);
}
#[test]
fn test_is_standard_unix_directory() {
assert!(is_standard_unix_directory("/bin/bash"));
assert!(is_standard_unix_directory("/usr/local/bin"));
assert!(!is_standard_unix_directory("/my/custom/path"));
}
#[test]
fn test_unix_path_ext() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test_file.txt");
File::create(&file_path).unwrap();
let ext = UnixPathExt::new(&file_path);
assert_eq!(ext.separator(), '/');
assert!(ext.is_absolute());
assert!(ext.is_accessible());
let attrs = ext.get_attributes().unwrap();
assert!(!attrs.is_directory);
assert!(!attrs.is_hidden);
assert_eq!(attrs.size, 0);
let hidden_path = temp_dir.path().join(".hidden");
File::create(&hidden_path).unwrap();
let hidden_ext = UnixPathExt::new(&hidden_path);
let hidden_attrs = hidden_ext.get_attributes().unwrap();
assert!(hidden_attrs.is_hidden);
}
}