cross_path/
converter.rs

1use crate::{PathConfig, PathError, PathResult, PathStyle};
2use regex::Regex;
3
4/// Path converter for Windows ↔ Unix conversion
5#[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    /// Create new path converter
15    ///
16    /// # Panics
17    ///
18    /// Panics if the internal regex patterns are invalid.
19    #[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    /// Convert path to specified style
30    ///
31    /// # Errors
32    ///
33    /// Returns `PathError` if the path cannot be converted or the format is unsupported.
34    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            // Even if styles match, we might want to normalize separators
39            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    /// Detect path style
56    ///
57    /// # Errors
58    ///
59    /// Returns `PathError` if detection fails (though currently it always succeeds or returns default).
60    pub fn detect_style(&self, path: &str) -> PathResult<PathStyle> {
61        // Check for Windows path
62        if self.windows_path_regex.is_match(path) {
63            return Ok(PathStyle::Windows);
64        }
65
66        // Check for Unix path
67        if self.unix_path_regex.is_match(path) {
68            return Ok(PathStyle::Unix);
69        }
70
71        // Relative path, detect by separator
72        if path.contains('\\') && !path.contains('/') {
73            Ok(PathStyle::Windows)
74        } else if path.contains('/') && !path.contains('\\') {
75            Ok(PathStyle::Unix)
76        } else {
77            // Mixed separators, try intelligent detection
78            if path.starts_with(r"\\") || path.contains(":\\") {
79                Ok(PathStyle::Windows)
80            } else if path.starts_with('/') {
81                Ok(PathStyle::Unix)
82            } else {
83                // Default to current platform style
84                Ok(super::platform::current_style())
85            }
86        }
87    }
88
89    /// Convert Windows path to Unix
90    fn windows_to_unix(&self, path: &str) -> PathResult<String> {
91        let normalized = self.normalize_windows_path(path);
92
93        // Handle UNC paths
94        if normalized.starts_with(r"\\") {
95            return Self::convert_unc_path(&normalized);
96        }
97
98        // Handle drive letter paths
99        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        // Handle relative paths
105        let unix_path = normalized.replace('\\', "/");
106        Ok(unix_path)
107    }
108
109    /// Convert Unix path to Windows
110    fn unix_to_windows(&self, path: &str) -> String {
111        let normalized = Self::normalize_unix_path(path);
112
113        // Check for UNC paths (Unix style //server/share)
114        if normalized.starts_with("//") {
115            return normalized.replace('/', "\\");
116        }
117
118        // Check for mapped drive paths
119        // Fix: Tuple is (Windows, Unix), so we must destructure as (windows_drive, unix_prefix)
120        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        // Handle regular Unix paths
128        #[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            // For absolute paths, map to default drive
144            return format!("C:{}", normalized.replace('/', "\\"));
145        }
146
147        // Relative paths
148        normalized.replace('/', "\\")
149    }
150
151    /// Normalize Windows path
152    fn normalize_windows_path(&self, path: &str) -> String {
153        let mut result = path.to_string();
154
155        // Unify separators
156        result = result.replace('/', "\\");
157
158        // Remove duplicate separators
159        while result.contains("\\\\") && !result.starts_with(r"\\") {
160            result = result.replace("\\\\", "\\");
161        }
162
163        // Remove trailing separator (unless root path)
164        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    /// Normalize Unix path
173    fn normalize_unix_path(path: &str) -> String {
174        let mut result = path.to_string();
175
176        // Unify separators
177        result = result.replace('\\', "/");
178
179        // Remove duplicate separators
180        while result.contains("//") && !result.starts_with("//") {
181            result = result.replace("//", "/");
182        }
183
184        // Remove trailing separator (unless root path)
185        if result.ends_with('/') && result != "/" {
186            result.pop();
187        }
188
189        result
190    }
191
192    /// Split drive letter from path
193    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    /// Map Windows drive letter to Unix path
205    fn map_drive_to_unix(&self, drive: &str, rest: &str) -> String {
206        // Look for mapping configuration
207        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        // Default mapping
214        let drive_letter = drive.chars().next().unwrap().to_ascii_lowercase();
215        format!("/mnt/{}{}", drive_letter, rest.replace('\\', "/"))
216    }
217
218    /// Convert UNC path
219    fn convert_unc_path(path: &str) -> PathResult<String> {
220        // UNC path format: \\server\share\path
221        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}