1use crate::{PathConfig, PathError, PathResult, PathStyle};
2use regex::Regex;
3
4#[derive(Debug, Clone)]
6pub struct PathConverter {
7 config: PathConfig,
8 windows_path_regex: Regex,
9 unix_path_regex: Regex,
10 drive_letter_regex: Regex,
11}
12
13impl PathConverter {
14 #[must_use]
20 pub fn new(config: &PathConfig) -> Self {
21 Self {
22 config: config.clone(),
23 windows_path_regex: Regex::new(r"^([a-zA-Z]:)([/\\].*)?$").unwrap(),
24 unix_path_regex: Regex::new(r"^/([^/].*)?$").unwrap(),
25 drive_letter_regex: Regex::new(r"^[a-zA-Z]:$").unwrap(),
26 }
27 }
28
29 pub fn convert(&self, path: &str, target_style: PathStyle) -> PathResult<String> {
35 let source_style = self.detect_style(path)?;
36
37 if source_style == target_style {
38 match target_style {
40 PathStyle::Windows => return Ok(self.normalize_windows_path(path)),
41 PathStyle::Unix => return Ok(Self::normalize_unix_path(path)),
42 PathStyle::Auto => return Ok(path.to_string()),
43 }
44 }
45
46 match (source_style, target_style) {
47 (PathStyle::Windows, PathStyle::Unix) => self.windows_to_unix(path),
48 (PathStyle::Unix, PathStyle::Windows) => Ok(self.unix_to_windows(path)),
49 _ => Err(PathError::UnsupportedFormat(format!(
50 "Unsupported conversion: {source_style:?} -> {target_style:?}"
51 ))),
52 }
53 }
54
55 pub fn detect_style(&self, path: &str) -> PathResult<PathStyle> {
61 if self.windows_path_regex.is_match(path) {
63 return Ok(PathStyle::Windows);
64 }
65
66 if self.unix_path_regex.is_match(path) {
68 return Ok(PathStyle::Unix);
69 }
70
71 if path.contains('\\') && !path.contains('/') {
73 Ok(PathStyle::Windows)
74 } else if path.contains('/') && !path.contains('\\') {
75 Ok(PathStyle::Unix)
76 } else {
77 if path.starts_with(r"\\") || path.contains(":\\") {
79 Ok(PathStyle::Windows)
80 } else if path.starts_with('/') {
81 Ok(PathStyle::Unix)
82 } else {
83 Ok(super::platform::current_style())
85 }
86 }
87 }
88
89 fn windows_to_unix(&self, path: &str) -> PathResult<String> {
91 let normalized = self.normalize_windows_path(path);
92
93 if normalized.starts_with(r"\\") {
95 return Self::convert_unc_path(&normalized);
96 }
97
98 if let Some((drive, rest)) = self.split_drive_path(&normalized) {
100 let unix_path = self.map_drive_to_unix(&drive, &rest);
101 return Ok(unix_path);
102 }
103
104 let unix_path = normalized.replace('\\', "/");
106 Ok(unix_path)
107 }
108
109 fn unix_to_windows(&self, path: &str) -> String {
111 let normalized = Self::normalize_unix_path(path);
112
113 if normalized.starts_with("//") {
115 return normalized.replace('/', "\\");
116 }
117
118 for (windows_drive, unix_prefix) in &self.config.drive_mappings {
121 if normalized.starts_with(unix_prefix) {
122 let rest = &normalized[unix_prefix.len()..];
123 return format!("{}{}", windows_drive, rest.replace('/', "\\"));
124 }
125 }
126
127 #[cfg(not(target_os = "windows"))]
129 if normalized.starts_with("/mnt/")
130 && let Some((drive, rest)) = crate::platform::unix::parse_unix_mount_point(&normalized)
131 {
132 let drive_str: String = drive.to_ascii_uppercase().clone();
133 let rest_str: String = rest.replace('/', "\\");
134 return format!(
135 "{}:{}{}",
136 drive_str,
137 rest_str,
138 if rest.is_empty() { "\\" } else { "" }
139 );
140 }
141
142 if normalized.starts_with('/') {
143 return format!("C:{}", normalized.replace('/', "\\"));
145 }
146
147 normalized.replace('/', "\\")
149 }
150
151 fn normalize_windows_path(&self, path: &str) -> String {
153 let mut result = path.to_string();
154
155 result = result.replace('/', "\\");
157
158 while result.contains("\\\\") && !result.starts_with(r"\\") {
160 result = result.replace("\\\\", "\\");
161 }
162
163 if result.ends_with('\\') && result.len() > 3 && !self.drive_letter_regex.is_match(&result)
165 {
166 result.pop();
167 }
168
169 result
170 }
171
172 fn normalize_unix_path(path: &str) -> String {
174 let mut result = path.to_string();
175
176 result = result.replace('\\', "/");
178
179 while result.contains("//") && !result.starts_with("//") {
181 result = result.replace("//", "/");
182 }
183
184 if result.ends_with('/') && result != "/" {
186 result.pop();
187 }
188
189 result
190 }
191
192 fn split_drive_path(&self, path: &str) -> Option<(String, String)> {
194 if path.len() >= 2 {
195 let drive = &path[..2];
196 if self.drive_letter_regex.is_match(drive) {
197 let rest = if path.len() > 2 { &path[2..] } else { "" };
198 return Some((drive.to_string(), rest.to_string()));
199 }
200 }
201 None
202 }
203
204 fn map_drive_to_unix(&self, drive: &str, rest: &str) -> String {
206 for (windows_drive, unix_mount) in &self.config.drive_mappings {
208 if windows_drive == drive {
209 return format!("{}{}", unix_mount, rest.replace('\\', "/"));
210 }
211 }
212
213 let drive_letter = drive.chars().next().unwrap().to_ascii_lowercase();
215 format!("/mnt/{}{}", drive_letter, rest.replace('\\', "/"))
216 }
217
218 fn convert_unc_path(path: &str) -> PathResult<String> {
220 let parts: Vec<&str> = path.split('\\').collect();
222 if parts.len() >= 4 {
223 let server = parts[2];
224 let share = parts[3];
225 let rest = if parts.len() > 4 {
226 parts[4..].join("/")
227 } else {
228 String::new()
229 };
230 let unix_path = format!("//{server}/{share}/{rest}");
231 return Ok(unix_path.trim_end_matches('/').to_string());
232 }
233
234 Err(PathError::ParseError(format!("Invalid UNC path: {path}")))
235 }
236}