cross_path/platform/
unix.rs1use crate::PathError;
9use crate::platform::{DiskInfo, FileAttributes, PathExt, PlatformPath};
10use std::fs;
11use std::path::{Path, PathBuf};
12
13pub struct UnixPathExt {
15 path: PathBuf,
16}
17
18impl UnixPathExt {
19 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 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#[must_use]
95pub fn is_absolute_unix_path(path: &str) -> bool {
96 path.starts_with('/')
97}
98
99#[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
128pub 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#[derive(Debug, Clone)]
150pub struct PathStats {
151 pub size: u64,
153 pub is_dir: bool,
155 pub permissions: fs::Permissions,
157 pub modified: Option<std::time::SystemTime>,
159 pub accessed: Option<std::time::SystemTime>,
161 pub created: Option<std::time::SystemTime>,
163}
164
165#[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
176pub 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#[derive(Debug, Clone)]
214pub struct FilesystemStats {
215 pub block_size: u64,
217 pub total_blocks: u64,
219 pub free_blocks: u64,
221 pub available_blocks: u64,
223 pub total_inodes: u64,
225 pub free_inodes: u64,
227 pub filesystem_id: u64,
229 pub mount_flags: u64,
231 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 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}