1use crate::PyException;
7use std::path::{Path as StdPath, PathBuf as StdPathBuf};
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub struct PurePath {
12 path: StdPathBuf,
13}
14
15impl PurePath {
16 pub fn new<P: AsRef<StdPath>>(path: P) -> Self {
18 Self {
19 path: path.as_ref().to_path_buf(),
20 }
21 }
22
23 pub fn parts(&self) -> Vec<String> {
25 self.path.components()
26 .map(|c| c.as_os_str().to_string_lossy().to_string())
27 .collect()
28 }
29
30 pub fn drive(&self) -> String {
32 #[cfg(windows)]
33 {
34 if let Some(prefix) = self.path.components().next() {
35 if let std::path::Component::Prefix(prefix_component) = prefix {
36 return prefix_component.as_os_str().to_string_lossy().to_string();
37 }
38 }
39 }
40 String::new()
41 }
42
43 pub fn root(&self) -> String {
45 if self.path.is_absolute() {
46 std::path::MAIN_SEPARATOR.to_string()
47 } else {
48 String::new()
49 }
50 }
51
52 pub fn anchor(&self) -> String {
54 format!("{}{}", self.drive(), self.root())
55 }
56
57 pub fn parent(&self) -> PurePath {
59 if let Some(parent) = self.path.parent() {
60 PurePath::new(parent)
61 } else {
62 PurePath::new("")
63 }
64 }
65
66 pub fn parents(&self) -> Vec<PurePath> {
68 let mut parents = Vec::new();
69 let mut current = self.path.as_path();
70
71 while let Some(parent) = current.parent() {
72 parents.push(PurePath::new(parent));
73 current = parent;
74 }
75
76 parents
77 }
78
79 pub fn name(&self) -> String {
81 self.path.file_name()
82 .map(|n| n.to_string_lossy().to_string())
83 .unwrap_or_default()
84 }
85
86 pub fn suffix(&self) -> String {
88 self.path.extension()
89 .map(|ext| format!(".{}", ext.to_string_lossy()))
90 .unwrap_or_default()
91 }
92
93 pub fn suffixes(&self) -> Vec<String> {
95 let name = self.name();
96 if name.is_empty() {
97 return Vec::new();
98 }
99
100 let mut suffixes = Vec::new();
101 let parts: Vec<&str> = name.split('.').collect();
102
103 if parts.len() > 1 {
104 for i in 1..parts.len() {
105 suffixes.push(format!(".{}", parts[i]));
106 }
107 }
108
109 suffixes
110 }
111
112 pub fn stem(&self) -> String {
114 self.path.file_stem()
115 .map(|stem| stem.to_string_lossy().to_string())
116 .unwrap_or_default()
117 }
118
119 pub fn joinpath<P: AsRef<StdPath>>(&self, other: P) -> PurePath {
121 PurePath::new(self.path.join(other))
122 }
123
124 pub fn match_pattern(&self, pattern: &str) -> bool {
126 let name = self.name();
128 match_glob(&name, pattern)
129 }
130
131 pub fn relative_to(&self, other: &PurePath) -> Result<PurePath, PyException> {
133 self.path.strip_prefix(&other.path)
134 .map(|p| PurePath::new(p))
135 .map_err(|_| crate::value_error("Path is not relative to the given path"))
136 }
137
138 pub fn is_absolute(&self) -> bool {
140 self.path.is_absolute()
141 }
142
143 pub fn is_relative(&self) -> bool {
145 self.path.is_relative()
146 }
147
148 pub fn as_posix(&self) -> String {
150 self.path.to_string_lossy().replace('\\', "/")
151 }
152
153 pub fn with_name<S: AsRef<str>>(&self, name: S) -> PurePath {
155 PurePath::new(self.path.with_file_name(name.as_ref()))
156 }
157
158 pub fn with_suffix<S: AsRef<str>>(&self, suffix: S) -> PurePath {
160 let suffix = suffix.as_ref();
161 let stem = self.stem();
162 if suffix.is_empty() {
163 PurePath::new(self.path.with_file_name(stem))
164 } else {
165 let new_name = if suffix.starts_with('.') {
166 format!("{}{}", stem, suffix)
167 } else {
168 format!("{}.{}", stem, suffix)
169 };
170 PurePath::new(self.path.with_file_name(new_name))
171 }
172 }
173}
174
175impl std::fmt::Display for PurePath {
176 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
177 write!(f, "{}", self.path.display())
178 }
179}
180
181impl From<&str> for PurePath {
182 fn from(s: &str) -> Self {
183 PurePath::new(s)
184 }
185}
186
187impl From<String> for PurePath {
188 fn from(s: String) -> Self {
189 PurePath::new(s)
190 }
191}
192
193#[derive(Debug, Clone)]
195pub struct Path {
196 pure_path: PurePath,
197}
198
199impl Path {
200 pub fn new<P: AsRef<StdPath>>(path: P) -> Self {
202 Self {
203 pure_path: PurePath::new(path),
204 }
205 }
206
207 #[cfg(feature = "std")]
209 pub fn cwd() -> Result<Path, PyException> {
210 std::env::current_dir()
211 .map(|p| Path::new(p))
212 .map_err(|e| crate::runtime_error(format!("Failed to get current directory: {}", e)))
213 }
214
215 #[cfg(feature = "std")]
217 pub fn home() -> Result<Path, PyException> {
218 std::env::var("HOME")
219 .or_else(|_| std::env::var("USERPROFILE"))
220 .map(|home_str| Path::new(home_str))
221 .map_err(|_| crate::runtime_error("Failed to get home directory"))
222 }
223
224 #[cfg(feature = "std")]
226 pub fn absolute(&self) -> Result<Path, PyException> {
227 self.pure_path.path.canonicalize()
228 .or_else(|_| {
229 if self.pure_path.path.is_absolute() {
230 Ok(self.pure_path.path.clone())
231 } else {
232 std::env::current_dir().map(|cwd| cwd.join(&self.pure_path.path))
233 }
234 })
235 .map(|p| Path::new(p))
236 .map_err(|e| crate::runtime_error(format!("Failed to get absolute path: {}", e)))
237 }
238
239 #[cfg(feature = "std")]
241 pub fn resolve(&self) -> Result<Path, PyException> {
242 self.pure_path.path.canonicalize()
243 .map(|p| Path::new(p))
244 .map_err(|e| crate::runtime_error(format!("Failed to resolve path: {}", e)))
245 }
246
247 #[cfg(feature = "std")]
249 pub fn exists(&self) -> bool {
250 self.pure_path.path.exists()
251 }
252
253 #[cfg(feature = "std")]
255 pub fn is_file(&self) -> bool {
256 self.pure_path.path.is_file()
257 }
258
259 #[cfg(feature = "std")]
261 pub fn is_dir(&self) -> bool {
262 self.pure_path.path.is_dir()
263 }
264
265 #[cfg(feature = "std")]
267 pub fn is_symlink(&self) -> bool {
268 self.pure_path.path.is_symlink()
269 }
270
271 #[cfg(feature = "std")]
273 pub fn stat(&self) -> Result<FileStats, PyException> {
274 self.pure_path.path.metadata()
275 .map(|meta| FileStats::from_metadata(meta))
276 .map_err(|e| crate::runtime_error(format!("Failed to get file stats: {}", e)))
277 }
278
279 #[cfg(feature = "std")]
281 pub fn mkdir(&self, parents: bool, exist_ok: bool) -> Result<(), PyException> {
282 if parents {
283 std::fs::create_dir_all(&self.pure_path.path)
284 } else {
285 std::fs::create_dir(&self.pure_path.path)
286 }.or_else(|e| {
287 if exist_ok && e.kind() == std::io::ErrorKind::AlreadyExists && self.is_dir() {
288 Ok(())
289 } else {
290 Err(e)
291 }
292 })
293 .map_err(|e| crate::runtime_error(format!("Failed to create directory: {}", e)))
294 }
295
296 #[cfg(feature = "std")]
298 pub fn rmdir(&self) -> Result<(), PyException> {
299 std::fs::remove_dir(&self.pure_path.path)
300 .map_err(|e| crate::runtime_error(format!("Failed to remove directory: {}", e)))
301 }
302
303 #[cfg(feature = "std")]
305 pub fn unlink(&self, missing_ok: bool) -> Result<(), PyException> {
306 std::fs::remove_file(&self.pure_path.path)
307 .or_else(|e| {
308 if missing_ok && e.kind() == std::io::ErrorKind::NotFound {
309 Ok(())
310 } else {
311 Err(e)
312 }
313 })
314 .map_err(|e| crate::runtime_error(format!("Failed to remove file: {}", e)))
315 }
316
317 #[cfg(feature = "std")]
319 pub fn iterdir(&self) -> Result<Vec<Path>, PyException> {
320 std::fs::read_dir(&self.pure_path.path)
321 .map_err(|e| crate::runtime_error(format!("Failed to read directory: {}", e)))
322 .map(|entries| {
323 entries.filter_map(|entry| {
324 entry.ok().map(|e| Path::new(e.path()))
325 }).collect()
326 })
327 }
328
329 #[cfg(feature = "std")]
331 pub fn glob(&self, pattern: &str) -> Result<Vec<Path>, PyException> {
332 let entries = self.iterdir()?;
334 let mut matches = Vec::new();
335
336 for entry in entries {
337 if entry.pure_path.match_pattern(pattern) {
338 matches.push(entry);
339 }
340 }
341
342 Ok(matches)
343 }
344
345 #[cfg(feature = "std")]
347 pub fn rglob(&self, pattern: &str) -> Result<Vec<Path>, PyException> {
348 let mut matches = Vec::new();
349 self.rglob_recursive(pattern, &mut matches)?;
350 Ok(matches)
351 }
352
353 #[cfg(feature = "std")]
354 fn rglob_recursive(&self, pattern: &str, matches: &mut Vec<Path>) -> Result<(), PyException> {
355 if let Ok(entries) = self.iterdir() {
356 for entry in entries {
357 if entry.pure_path.match_pattern(pattern) {
358 matches.push(entry.clone());
359 }
360 if entry.is_dir() {
361 entry.rglob_recursive(pattern, matches)?;
362 }
363 }
364 }
365 Ok(())
366 }
367
368 #[cfg(feature = "std")]
370 pub fn read_text(&self, _encoding: Option<&str>) -> Result<String, PyException> {
371 std::fs::read_to_string(&self.pure_path.path)
372 .map_err(|e| crate::runtime_error(format!("Failed to read text file: {}", e)))
373 }
374
375 #[cfg(feature = "std")]
377 pub fn write_text<S: AsRef<str>>(&self, data: S) -> Result<(), PyException> {
378 std::fs::write(&self.pure_path.path, data.as_ref())
379 .map_err(|e| crate::runtime_error(format!("Failed to write text file: {}", e)))
380 }
381
382 #[cfg(feature = "std")]
384 pub fn read_bytes(&self) -> Result<Vec<u8>, PyException> {
385 std::fs::read(&self.pure_path.path)
386 .map_err(|e| crate::runtime_error(format!("Failed to read bytes: {}", e)))
387 }
388
389 #[cfg(feature = "std")]
391 pub fn write_bytes(&self, data: &[u8]) -> Result<(), PyException> {
392 std::fs::write(&self.pure_path.path, data)
393 .map_err(|e| crate::runtime_error(format!("Failed to write bytes: {}", e)))
394 }
395}
396
397impl std::ops::Deref for Path {
399 type Target = PurePath;
400
401 fn deref(&self) -> &Self::Target {
402 &self.pure_path
403 }
404}
405
406impl std::fmt::Display for Path {
407 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
408 write!(f, "{}", self.pure_path)
409 }
410}
411
412impl From<&str> for Path {
413 fn from(s: &str) -> Self {
414 Path::new(s)
415 }
416}
417
418impl From<String> for Path {
419 fn from(s: String) -> Self {
420 Path::new(s)
421 }
422}
423
424#[derive(Debug, Clone)]
426pub struct FileStats {
427 pub size: u64,
428 pub is_dir: bool,
429 pub is_file: bool,
430 pub is_symlink: bool,
431 #[cfg(feature = "std")]
432 pub modified: Option<std::time::SystemTime>,
433 #[cfg(feature = "std")]
434 pub accessed: Option<std::time::SystemTime>,
435 #[cfg(feature = "std")]
436 pub created: Option<std::time::SystemTime>,
437}
438
439#[cfg(feature = "std")]
440impl FileStats {
441 fn from_metadata(metadata: std::fs::Metadata) -> Self {
442 Self {
443 size: metadata.len(),
444 is_dir: metadata.is_dir(),
445 is_file: metadata.is_file(),
446 is_symlink: metadata.is_symlink(),
447 modified: metadata.modified().ok(),
448 accessed: metadata.accessed().ok(),
449 created: metadata.created().ok(),
450 }
451 }
452}
453
454pub fn pure_path<P: AsRef<StdPath>>(path: P) -> PurePath {
458 PurePath::new(path)
459}
460
461pub fn path<P: AsRef<StdPath>>(p: P) -> Path {
463 Path::new(p)
464}
465
466fn match_glob(text: &str, pattern: &str) -> bool {
469 let pattern_chars: Vec<char> = pattern.chars().collect();
471 let text_chars: Vec<char> = text.chars().collect();
472
473 match_glob_recursive(&text_chars, &pattern_chars, 0, 0)
474}
475
476fn match_glob_recursive(text: &[char], pattern: &[char], text_idx: usize, pattern_idx: usize) -> bool {
477 if pattern_idx >= pattern.len() {
479 return text_idx >= text.len();
480 }
481
482 if text_idx >= text.len() {
483 return pattern[pattern_idx..].iter().all(|&c| c == '*');
485 }
486
487 match pattern[pattern_idx] {
488 '*' => {
489 match_glob_recursive(text, pattern, text_idx, pattern_idx + 1) ||
491 match_glob_recursive(text, pattern, text_idx + 1, pattern_idx)
492 }
493 '?' => {
494 match_glob_recursive(text, pattern, text_idx + 1, pattern_idx + 1)
496 }
497 c => {
498 if text[text_idx] == c {
500 match_glob_recursive(text, pattern, text_idx + 1, pattern_idx + 1)
501 } else {
502 false
503 }
504 }
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511
512 #[test]
513 fn test_purepath_parts() {
514 let p = PurePath::new("/home/user/file.txt");
515 let parts = p.parts();
516 assert!(parts.contains(&"home".to_string()));
517 assert!(parts.contains(&"user".to_string()));
518 assert!(parts.contains(&"file.txt".to_string()));
519 }
520
521 #[test]
522 fn test_purepath_suffix() {
523 let p = PurePath::new("file.tar.gz");
524 assert_eq!(p.suffix(), ".gz");
525 assert_eq!(p.suffixes(), vec![".tar", ".gz"]);
526 assert_eq!(p.stem(), "file.tar");
527 }
528
529 #[test]
530 fn test_purepath_joinpath() {
531 let p1 = PurePath::new("/home/user");
532 let p2 = p1.joinpath("documents");
533 assert_eq!(p2.name(), "documents");
534 }
535
536 #[test]
537 fn test_glob_matching() {
538 assert!(match_glob("file.txt", "*.txt"));
539 assert!(match_glob("test.py", "test.*"));
540 assert!(match_glob("hello", "h?llo"));
541 assert!(!match_glob("file.py", "*.txt"));
542 }
543}