stdpython/stdlib/
pathlib.rs

1//! Python pathlib module implementation
2//! 
3//! This module provides object-oriented filesystem paths.
4//! Implementation matches Python's pathlib module API.
5
6use crate::PyException;
7use std::path::{Path as StdPath, PathBuf as StdPathBuf};
8
9/// Pure path - platform-independent path operations
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub struct PurePath {
12    path: StdPathBuf,
13}
14
15impl PurePath {
16    /// Create new PurePath
17    pub fn new<P: AsRef<StdPath>>(path: P) -> Self {
18        Self {
19            path: path.as_ref().to_path_buf(),
20        }
21    }
22    
23    /// Get path parts
24    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    /// Get drive (Windows only)
31    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    /// Get root
44    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    /// Get anchor (drive + root)
53    pub fn anchor(&self) -> String {
54        format!("{}{}", self.drive(), self.root())
55    }
56    
57    /// Get parent directory
58    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    /// Get all parents
67    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    /// Get file name
80    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    /// Get file suffix
87    pub fn suffix(&self) -> String {
88        self.path.extension()
89            .map(|ext| format!(".{}", ext.to_string_lossy()))
90            .unwrap_or_default()
91    }
92    
93    /// Get all suffixes
94    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    /// Get stem (filename without final suffix)
113    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    /// Join with other path
120    pub fn joinpath<P: AsRef<StdPath>>(&self, other: P) -> PurePath {
121        PurePath::new(self.path.join(other))
122    }
123    
124    /// Check if path matches pattern
125    pub fn match_pattern(&self, pattern: &str) -> bool {
126        // Simple glob-like matching
127        let name = self.name();
128        match_glob(&name, pattern)
129    }
130    
131    /// Get relative path to other
132    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    /// Check if path is absolute
139    pub fn is_absolute(&self) -> bool {
140        self.path.is_absolute()
141    }
142    
143    /// Check if path is relative
144    pub fn is_relative(&self) -> bool {
145        self.path.is_relative()
146    }
147    
148    /// Convert to string
149    pub fn as_posix(&self) -> String {
150        self.path.to_string_lossy().replace('\\', "/")
151    }
152    
153    /// Replace path components
154    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    /// Replace suffix
159    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/// Concrete path - includes filesystem operations
194#[derive(Debug, Clone)]
195pub struct Path {
196    pure_path: PurePath,
197}
198
199impl Path {
200    /// Create new Path
201    pub fn new<P: AsRef<StdPath>>(path: P) -> Self {
202        Self {
203            pure_path: PurePath::new(path),
204        }
205    }
206    
207    /// Current working directory
208    #[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    /// Home directory
216    #[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    /// Get absolute path
225    #[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    /// Resolve path (follow symlinks)
240    #[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    /// Check if path exists
248    #[cfg(feature = "std")]
249    pub fn exists(&self) -> bool {
250        self.pure_path.path.exists()
251    }
252    
253    /// Check if path is file
254    #[cfg(feature = "std")]
255    pub fn is_file(&self) -> bool {
256        self.pure_path.path.is_file()
257    }
258    
259    /// Check if path is directory
260    #[cfg(feature = "std")]
261    pub fn is_dir(&self) -> bool {
262        self.pure_path.path.is_dir()
263    }
264    
265    /// Check if path is symlink
266    #[cfg(feature = "std")]
267    pub fn is_symlink(&self) -> bool {
268        self.pure_path.path.is_symlink()
269    }
270    
271    /// Get file statistics
272    #[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    /// Create directory
280    #[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    /// Remove directory
297    #[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    /// Remove file
304    #[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    /// List directory contents
318    #[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    /// Glob pattern matching
330    #[cfg(feature = "std")]
331    pub fn glob(&self, pattern: &str) -> Result<Vec<Path>, PyException> {
332        // Simple implementation - in a real implementation you'd want to use a proper glob library
333        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    /// Recursive glob
346    #[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    /// Read text file
369    #[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    /// Write text file
376    #[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    /// Read bytes
383    #[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    /// Write bytes
390    #[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
397// Delegate PurePath methods to Path
398impl 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/// File statistics
425#[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
454// Module-level functions
455
456/// Create PurePath
457pub fn pure_path<P: AsRef<StdPath>>(path: P) -> PurePath {
458    PurePath::new(path)
459}
460
461/// Create Path
462pub fn path<P: AsRef<StdPath>>(p: P) -> Path {
463    Path::new(p)
464}
465
466// Helper functions
467
468fn match_glob(text: &str, pattern: &str) -> bool {
469    // Simple glob matching - supports * and ?
470    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    // Base cases
478    if pattern_idx >= pattern.len() {
479        return text_idx >= text.len();
480    }
481    
482    if text_idx >= text.len() {
483        // Check if remaining pattern is only '*'
484        return pattern[pattern_idx..].iter().all(|&c| c == '*');
485    }
486    
487    match pattern[pattern_idx] {
488        '*' => {
489            // Try matching zero or more characters
490            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 exactly one character
495            match_glob_recursive(text, pattern, text_idx + 1, pattern_idx + 1)
496        }
497        c => {
498            // Match exact character
499            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}