cross_path/
parser.rs

1use crate::PathResult;
2use regex::Regex;
3use std::path::{Path, PathBuf};
4
5/// Path parser for analyzing path structure
6#[derive(Debug, Clone)]
7pub struct PathParser {
8    windows_absolute: Regex,
9    unix_absolute: Regex,
10    unc_path: Regex,
11}
12
13impl Default for PathParser {
14    fn default() -> Self {
15        Self {
16            windows_absolute: Regex::new(r"^[a-zA-Z]:[/\\].*$").unwrap(),
17            unix_absolute: Regex::new(r"^/.*$").unwrap(),
18            unc_path: Regex::new(r"^\\\\[^\\]+\\[^\\]+").unwrap(),
19        }
20    }
21}
22
23impl PathParser {
24    /// Create new path parser
25    #[must_use]
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    /// Parse path into structured components
31    ///
32    /// # Errors
33    ///
34    /// Returns `PathError` if parsing fails (though currently it always succeeds).
35    pub fn parse(path: &str) -> PathResult<ParsedPath> {
36        let parser = Self::new();
37        Ok(parser.parse_internal(path))
38    }
39
40    fn parse_internal(&self, path: &str) -> ParsedPath {
41        let mut parsed = ParsedPath {
42            original: path.to_string(),
43            components: Vec::new(),
44            is_absolute: false,
45            has_drive: false,
46            drive_letter: None,
47            is_unc: false,
48            server: None,
49            share: None,
50        };
51
52        // Detect UNC path
53        if self.unc_path.is_match(path) {
54            parsed.is_unc = true;
55            if let Some((server, share)) = Self::parse_unc_path(path) {
56                parsed.server = Some(server);
57                parsed.share = Some(share);
58            }
59            parsed.is_absolute = true;
60            return parsed;
61        }
62
63        // Detect Windows absolute path
64        if self.windows_absolute.is_match(path) {
65            parsed.is_absolute = true;
66            parsed.has_drive = true;
67            parsed.drive_letter = Some(path.chars().next().unwrap().to_ascii_uppercase());
68
69            // Parse components
70            let normalized = path.replace('\\', "/");
71            let components: Vec<&str> = normalized.split('/').filter(|s| !s.is_empty()).collect();
72            parsed.components = components.into_iter().map(String::from).collect();
73
74            return parsed;
75        }
76
77        // Detect Unix absolute path
78        if self.unix_absolute.is_match(path) {
79            parsed.is_absolute = true;
80
81            let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
82            parsed.components = components.into_iter().map(String::from).collect();
83
84            return parsed;
85        }
86
87        // Relative path
88        let components: Vec<&str> = path.split(['/', '\\']).filter(|s| !s.is_empty()).collect();
89        parsed.components = components.into_iter().map(String::from).collect();
90
91        parsed
92    }
93
94    fn parse_unc_path(path: &str) -> Option<(String, String)> {
95        let parts: Vec<&str> = path.split('\\').filter(|s| !s.is_empty()).collect();
96        if parts.len() >= 2 {
97            return Some((parts[0].to_string(), parts[1].to_string()));
98        }
99        None
100    }
101
102    /// Detect path style
103    #[must_use]
104    pub fn detect_style(path: &str) -> super::PathStyle {
105        let parser = Self::new();
106
107        if parser.unc_path.is_match(path) || parser.windows_absolute.is_match(path) {
108            super::PathStyle::Windows
109        } else if parser.unix_absolute.is_match(path) {
110            super::PathStyle::Unix
111        } else if path.contains('\\') && !path.contains('/') {
112            super::PathStyle::Windows
113        } else if path.contains('/') && !path.contains('\\') {
114            super::PathStyle::Unix
115        } else {
116            super::platform::current_style()
117        }
118    }
119
120    /// Normalize path by removing redundant components
121    ///
122    /// # Errors
123    ///
124    /// Returns `PathError` if the path is invalid.
125    ///
126    /// # Panics
127    ///
128    /// Panics if path components are invalid or inconsistent.
129    pub fn normalize_path(path: &Path) -> PathResult<PathBuf> {
130        let mut components = Vec::new();
131
132        for component in path.components() {
133            match component {
134                std::path::Component::Prefix(_) | std::path::Component::RootDir => {
135                    components.clear();
136                    components.push(component);
137                }
138                std::path::Component::CurDir => {
139                    // Ignore current directory
140                }
141                std::path::Component::ParentDir => {
142                    if components.is_empty() {
143                        components.push(component);
144                    } else {
145                        let last = components.last().unwrap();
146                        match last {
147                            std::path::Component::ParentDir
148                            | std::path::Component::RootDir
149                            | std::path::Component::Prefix(_) => {
150                                components.push(component);
151                            }
152                            _ => {
153                                components.pop();
154                            }
155                        }
156                    }
157                }
158                std::path::Component::Normal(name) => {
159                    components.push(std::path::Component::Normal(name));
160                }
161            }
162        }
163
164        let mut normalized = PathBuf::new();
165        for component in components {
166            normalized.push(component.as_os_str());
167        }
168
169        Ok(normalized)
170    }
171}
172
173/// Parsed path information
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct ParsedPath {
176    /// Original path string
177    pub original: String,
178    /// Path components
179    pub components: Vec<String>,
180    /// Whether path is absolute
181    pub is_absolute: bool,
182    /// Whether path has drive letter
183    pub has_drive: bool,
184    /// Drive letter (if present)
185    pub drive_letter: Option<char>,
186    /// Whether path is UNC
187    pub is_unc: bool,
188    /// UNC server name
189    pub server: Option<String>,
190    /// UNC share name
191    pub share: Option<String>,
192}