1use crate::PathResult;
2use regex::Regex;
3use std::path::{Path, PathBuf};
4
5#[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 #[must_use]
26 pub fn new() -> Self {
27 Self::default()
28 }
29
30 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 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 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 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 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 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 #[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 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 }
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#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct ParsedPath {
176 pub original: String,
178 pub components: Vec<String>,
180 pub is_absolute: bool,
182 pub has_drive: bool,
184 pub drive_letter: Option<char>,
186 pub is_unc: bool,
188 pub server: Option<String>,
190 pub share: Option<String>,
192}