cross_path/platform/
unix.rs

1//! Unix-specific path handling implementation
2//!
3//! This module provides Unix-specific implementations for path operations,
4//! including file attribute retrieval, filesystem statistics, and path normalization.
5//!
6//! It uses POSIX standard APIs (via `libc`) to interact with the underlying system.
7
8use crate::PathError;
9use crate::platform::{DiskInfo, FileAttributes, PathExt, PlatformPath};
10use std::fs;
11use std::path::{Path, PathBuf};
12
13/// Unix platform path extension
14pub struct UnixPathExt {
15    path: PathBuf,
16}
17
18impl UnixPathExt {
19    /// Create new `UnixPathExt`
20    pub fn new<P: AsRef<Path>>(path: P) -> Self {
21        Self {
22            path: path.as_ref().to_path_buf(),
23        }
24    }
25}
26
27impl PlatformPath for UnixPathExt {
28    fn separator(&self) -> char {
29        '/'
30    }
31
32    fn is_absolute(&self) -> bool {
33        self.path.is_absolute()
34    }
35
36    fn to_platform_specific(&self) -> String {
37        self.path.to_string_lossy().into_owned()
38    }
39}
40
41impl PathExt for UnixPathExt {
42    fn get_attributes(&self) -> Option<FileAttributes> {
43        let metadata = fs::metadata(&self.path).ok()?;
44
45        let size = metadata.len();
46        let is_directory = metadata.is_dir();
47        let is_readonly = metadata.permissions().readonly();
48
49        // Unix hidden file convention: starts with '.'
50        let is_hidden = self
51            .path
52            .file_name()
53            .and_then(|n| n.to_str())
54            .is_some_and(|s| s.starts_with('.'));
55
56        let creation_time = metadata
57            .created()
58            .ok()
59            .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
60            .map(|d| d.as_secs());
61
62        let modification_time = metadata
63            .modified()
64            .ok()
65            .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
66            .map(|d| d.as_secs());
67
68        Some(FileAttributes {
69            size,
70            is_directory,
71            is_hidden,
72            is_readonly,
73            creation_time,
74            modification_time,
75        })
76    }
77
78    fn is_accessible(&self) -> bool {
79        self.path.exists()
80    }
81
82    fn get_disk_info(&self) -> Option<DiskInfo> {
83        let stats = get_filesystem_stats(&self.path).ok()?;
84
85        Some(DiskInfo {
86            total_space: stats.total_blocks.saturating_mul(stats.block_size),
87            free_space: stats.available_blocks.saturating_mul(stats.block_size),
88            filesystem_type: "Unix".to_string(),
89        })
90    }
91}
92
93/// Check if string is an absolute Unix path
94#[must_use]
95pub fn is_absolute_unix_path(path: &str) -> bool {
96    path.starts_with('/')
97}
98
99/// Parse Unix mount point from path
100///
101/// # Panics
102///
103/// Panics if the path contains invalid characters or structure that cannot be parsed.
104#[must_use]
105pub fn parse_unix_mount_point(path: &str) -> Option<(&str, &str)> {
106    if let Some(stripped) = path.strip_prefix("/mnt/")
107        && let Some(pos) = stripped.find('/')
108    {
109        let drive = &stripped[..pos];
110        let rest = &stripped[pos..];
111        return Some((drive, rest));
112    }
113
114    if let Some(stripped) = path.strip_prefix('/')
115        && let Some(pos) = stripped.find('/')
116    {
117        let first_component = &stripped[..pos];
118        if first_component.len() == 1
119            && first_component.chars().next().unwrap().is_ascii_lowercase()
120        {
121            return Some((first_component, &stripped[pos..]));
122        }
123    }
124
125    None
126}
127
128/// Get Unix path statistics
129///
130/// # Arguments
131/// * `path` - Path to retrieve statistics for
132///
133/// # Errors
134/// Returns `PathError` if unable to retrieve metadata
135pub fn get_unix_path_stats(path: &Path) -> Result<PathStats, PathError> {
136    let metadata = fs::metadata(path)?;
137
138    Ok(PathStats {
139        size: metadata.len(),
140        is_dir: metadata.is_dir(),
141        permissions: metadata.permissions(),
142        modified: metadata.modified().ok(),
143        accessed: metadata.accessed().ok(),
144        created: metadata.created().ok(),
145    })
146}
147
148/// Unix path statistics structure
149#[derive(Debug, Clone)]
150pub struct PathStats {
151    /// File size in bytes
152    pub size: u64,
153    /// Whether the path is a directory
154    pub is_dir: bool,
155    /// File permissions
156    pub permissions: fs::Permissions,
157    /// Last modification time
158    pub modified: Option<std::time::SystemTime>,
159    /// Last access time
160    pub accessed: Option<std::time::SystemTime>,
161    /// Creation time
162    pub created: Option<std::time::SystemTime>,
163}
164
165/// Check if path is in standard Unix directories
166#[must_use]
167pub fn is_standard_unix_directory(path: &str) -> bool {
168    let standard_dirs = vec![
169        "/bin", "/boot", "/dev", "/etc", "/home", "/lib", "/lib64", "/media", "/mnt", "/opt",
170        "/proc", "/root", "/run", "/sbin", "/srv", "/sys", "/tmp", "/usr", "/var",
171    ];
172
173    standard_dirs.iter().any(|&dir| path.starts_with(dir))
174}
175
176/// Get filesystem statistics for Unix path
177///
178/// # Errors
179///
180/// Returns `PathError` if the filesystem statistics cannot be retrieved.
181pub fn get_filesystem_stats(path: &Path) -> Result<FilesystemStats, PathError> {
182    let path_cstr = std::ffi::CString::new(path.to_string_lossy().as_ref())
183        .map_err(|e| PathError::platform_error(e.to_string()))?;
184
185    let mut statfs: libc::statvfs = unsafe { std::mem::zeroed() };
186
187    unsafe {
188        if libc::statvfs(path_cstr.as_ptr(), &raw mut statfs) != 0 {
189            return Err(PathError::platform_error(format!(
190                "Failed to get filesystem stats for {}",
191                path.display()
192            )));
193        }
194    }
195
196    #[allow(clippy::unnecessary_cast)]
197    {
198        Ok(FilesystemStats {
199            block_size: statfs.f_bsize as u64,
200            total_blocks: statfs.f_blocks as u64,
201            free_blocks: statfs.f_bfree as u64,
202            available_blocks: statfs.f_bavail as u64,
203            total_inodes: statfs.f_files as u64,
204            free_inodes: statfs.f_ffree as u64,
205            filesystem_id: statfs.f_fsid as u64,
206            mount_flags: statfs.f_flag as u64,
207            max_filename_length: statfs.f_namemax as u64,
208        })
209    }
210}
211
212/// Filesystem statistics structure
213#[derive(Debug, Clone)]
214pub struct FilesystemStats {
215    /// Filesystem block size
216    pub block_size: u64,
217    /// Total number of blocks
218    pub total_blocks: u64,
219    /// Number of free blocks
220    pub free_blocks: u64,
221    /// Number of available blocks for unprivileged users
222    pub available_blocks: u64,
223    /// Total number of inodes
224    pub total_inodes: u64,
225    /// Number of free inodes
226    pub free_inodes: u64,
227    /// Filesystem ID
228    pub filesystem_id: u64,
229    /// Mount flags
230    pub mount_flags: u64,
231    /// Maximum filename length
232    pub max_filename_length: u64,
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use std::fs::File;
239    use tempfile::TempDir;
240
241    #[test]
242    fn test_is_absolute_unix_path() {
243        assert!(is_absolute_unix_path("/home/user"));
244        assert!(is_absolute_unix_path("/"));
245        assert!(!is_absolute_unix_path("relative/path"));
246        assert!(!is_absolute_unix_path("./relative"));
247    }
248
249    #[test]
250    fn test_parse_unix_mount_point() {
251        assert_eq!(
252            parse_unix_mount_point("/mnt/c/Users"),
253            Some(("c", "/Users"))
254        );
255        assert_eq!(parse_unix_mount_point("/c/Users"), Some(("c", "/Users")));
256        assert_eq!(parse_unix_mount_point("/home/user"), None);
257    }
258
259    #[test]
260    fn test_is_standard_unix_directory() {
261        assert!(is_standard_unix_directory("/bin/bash"));
262        assert!(is_standard_unix_directory("/usr/local/bin"));
263        assert!(!is_standard_unix_directory("/my/custom/path"));
264    }
265
266    #[test]
267    fn test_unix_path_ext() {
268        let temp_dir = TempDir::new().unwrap();
269        let file_path = temp_dir.path().join("test_file.txt");
270        File::create(&file_path).unwrap();
271
272        let ext = UnixPathExt::new(&file_path);
273
274        assert_eq!(ext.separator(), '/');
275        assert!(ext.is_absolute());
276        assert!(ext.is_accessible());
277
278        let attrs = ext.get_attributes().unwrap();
279        assert!(!attrs.is_directory);
280        assert!(!attrs.is_hidden);
281        assert_eq!(attrs.size, 0);
282
283        // Test hidden file
284        let hidden_path = temp_dir.path().join(".hidden");
285        File::create(&hidden_path).unwrap();
286        let hidden_ext = UnixPathExt::new(&hidden_path);
287        let hidden_attrs = hidden_ext.get_attributes().unwrap();
288        assert!(hidden_attrs.is_hidden);
289    }
290}