client_core/container/
path_utils.rs1use crate::container::environment::{HostOs, PathFormat};
6use std::path::{Path, PathBuf};
7use tracing::debug;
8
9#[derive(Debug, thiserror::Error)]
11pub enum PathUtilsError {
12 #[error("Path processing error: {0}")]
13 InvalidPath(String),
14}
15
16#[derive(Debug, Clone)]
18pub struct PathProcessor {
19 pub host_os: HostOs,
20 pub path_format: PathFormat,
21}
22
23impl PathProcessor {
24 pub fn new(host_os: HostOs, path_format: PathFormat) -> Self {
26 Self {
27 host_os,
28 path_format,
29 }
30 }
31
32 pub fn normalize_path(&self, input_path: &str) -> Result<String, PathUtilsError> {
35 let path = input_path.trim();
36
37 if path.is_empty() {
38 return Err(PathUtilsError::InvalidPath("Path cannot be empty".to_string()));
39 }
40
41 debug!(
42 "🔍 Normalizing path: '{}' (current environment: {:?})",
43 path, self.path_format
44 );
45
46 let cleaned_path = self.clean_path(path);
48
49 let formatted_path = match self.path_format {
51 PathFormat::Wsl2 => self.to_wsl2_format(&cleaned_path),
52 PathFormat::Windows => self.to_windows_format(&cleaned_path),
53 PathFormat::Posix => self.to_posix_format(&cleaned_path),
54 };
55
56 debug!("✅ Path normalization complete: '{}'", formatted_path);
57 Ok(formatted_path)
58 }
59
60 pub fn is_bind_mount_path(&self, path: &str) -> bool {
62 let path = path.trim();
63
64 if path.is_empty() {
65 return false;
66 }
67
68 if path.starts_with('/') && !path.starts_with("//") {
70 return true;
71 }
72
73 if path.len() >= 3
75 && path.chars().nth(1).unwrap_or_default() == ':'
76 && (path.chars().nth(2).unwrap_or_default() == '\\'
77 || path.chars().nth(2).unwrap_or_default() == '/')
78 {
79 return true;
80 }
81
82 if path.starts_with("/mnt/") || path.starts_with("/c/") || path.starts_with("/d/") {
84 return true;
85 }
86
87 if path.contains('/') || path.contains('\\') {
89 return !Path::new(path).file_name().map_or(false, |f| {
91 !Path::new(f).extension().map_or(false, |ext| {
92 ext.len() > 0
94 })
95 });
96 }
97
98 false
99 }
100
101 pub fn to_absolute_path(&self, path: &str, work_dir: &Path) -> Result<PathBuf, PathUtilsError> {
103 let normalized_path = self.normalize_path(path)?;
104 let path_buf = PathBuf::from(&normalized_path);
105
106 if path_buf.is_absolute() {
107 Ok(path_buf)
108 } else {
109 let absolute = work_dir.join(path_buf);
111 Ok(absolute)
112 }
113 }
114
115 fn clean_path(&self, path: &str) -> String {
117 let mut components = Vec::new();
118
119 for component in Path::new(path).components() {
120 match component {
121 std::path::Component::CurDir => {
122 continue;
124 }
125 std::path::Component::ParentDir => {
126 if let Some(last) = components.last() {
128 if last != &std::path::Component::RootDir {
129 components.pop();
130 }
131 }
132 }
133 _ => {
134 components.push(component);
135 }
136 }
137 }
138
139 let cleaned = components
140 .iter()
141 .map(|c| c.as_os_str().to_string_lossy())
142 .collect::<Vec<_>>()
143 .join(std::path::MAIN_SEPARATOR_STR);
144
145 if cleaned.is_empty() {
147 ".".to_string()
148 } else {
149 cleaned
150 }
151 }
152
153 fn to_wsl2_format(&self, path: &str) -> String {
155 let path = path.replace('\\', "/");
156
157 if path.len() >= 3
159 && path.chars().nth(1).unwrap_or_default() == ':'
160 && (path.chars().nth(2).unwrap_or_default() == '/'
161 || path.chars().nth(2).unwrap_or_default() == '\\')
162 {
163 let drive_letter = path
164 .chars()
165 .nth(0)
166 .unwrap_or_default()
167 .to_lowercase()
168 .next()
169 .unwrap_or_default();
170 let rest = &path[3..];
171 return format!("/mnt/{}{}", drive_letter, rest);
172 }
173
174 if path.starts_with("/mnt/") || path.starts_with("/c/") || path.starts_with("/d/") {
176 return path;
177 }
178
179 path
181 }
182
183 fn to_windows_format(&self, path: &str) -> String {
185 let path = path.replace('/', "\\");
186
187 if path.starts_with("/mnt/") {
189 let rest = &path[5..]; if !rest.is_empty() {
191 let drive_letter = rest
192 .chars()
193 .next()
194 .unwrap_or_default()
195 .to_uppercase()
196 .next()
197 .unwrap_or_default();
198 let rest = &rest[1..];
199 return format!("{}:\\{}", drive_letter, rest);
200 }
201 }
202
203 if path.starts_with("/c/") {
204 return format!("C:\\{}", &path[3..]);
205 }
206
207 if path.starts_with("/d/") {
208 return format!("D:\\{}", &path[3..]);
209 }
210
211 path
212 }
213
214 fn to_posix_format(&self, path: &str) -> String {
216 let path = path.replace('\\', "/");
217
218 if path.len() >= 3
220 && path.chars().nth(1).unwrap_or_default() == ':'
221 && (path.chars().nth(2).unwrap_or_default() == '\\'
222 || path.chars().nth(2).unwrap_or_default() == '/')
223 {
224 let drive_letter = path
225 .chars()
226 .nth(0)
227 .unwrap_or_default()
228 .to_lowercase()
229 .next()
230 .unwrap_or_default();
231 let rest = &path[3..];
232 return format!("/mnt/{}{}", drive_letter, rest);
233 }
234
235 if path.starts_with("/mnt/") || path.starts_with("/c/") || path.starts_with("/d/") {
237 return path;
238 }
239
240 path
241 }
242
243 pub fn convert_separators(&self, path: &str) -> String {
245 match self.path_format {
246 PathFormat::Wsl2 | PathFormat::Posix => path.replace('\\', "/"),
247 PathFormat::Windows => path.replace('/', "\\"),
248 }
249 }
250
251 pub fn needs_special_handling(&self, path: &str) -> bool {
253 self.is_bind_mount_path(path)
255 && (self.path_format == PathFormat::Wsl2 || self.path_format == PathFormat::Windows)
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 #[test]
264 fn test_normalize_path_wsl2() {
265 let processor = PathProcessor::new(HostOs::WindowsWsl2, PathFormat::Wsl2);
266
267 assert_eq!(
269 processor.normalize_path(r"C:\Users\test\data").unwrap(),
270 "/mnt/c/Users/test/data"
271 );
272
273 assert_eq!(processor.normalize_path("./data").unwrap(), "data");
275
276 assert_eq!(
278 processor.normalize_path("/mnt/c/Users/test").unwrap(),
279 "/mnt/c/Users/test"
280 );
281 }
282
283 #[test]
284 fn test_normalize_path_windows() {
285 let processor = PathProcessor::new(HostOs::WindowsNative, PathFormat::Windows);
286
287 assert_eq!(
289 processor.normalize_path("/mnt/c/Users/test").unwrap(),
290 r"C:\Users\test"
291 );
292
293 assert_eq!(
295 processor.normalize_path("C:/Users/test").unwrap(),
296 r"C:\Users\test"
297 );
298 }
299
300 #[test]
301 fn test_is_bind_mount_path_wsl2() {
302 let processor = PathProcessor::new(HostOs::WindowsWsl2, PathFormat::Wsl2);
303
304 assert!(processor.is_bind_mount_path("/mnt/c/Users/test/data"));
305 assert!(processor.is_bind_mount_path("/c/Users/test/data"));
306 assert!(processor.is_bind_mount_path("/data/mysql"));
307 assert!(processor.is_bind_mount_path("./data"));
308 assert!(processor.is_bind_mount_path("../data"));
309 assert!(processor.is_bind_mount_path(r"C:\data"));
310
311 assert!(!processor.is_bind_mount_path("volume_name"));
313 assert!(!processor.is_bind_mount_path(""));
314 }
315
316 #[test]
317 fn test_to_absolute_path() {
318 let processor = PathProcessor::new(HostOs::LinuxNative, PathFormat::Posix);
319 let work_dir = PathBuf::from("/workspace");
320
321 assert_eq!(
323 processor.to_absolute_path("/data", &work_dir).unwrap(),
324 PathBuf::from("/data")
325 );
326
327 assert_eq!(
329 processor.to_absolute_path("./data", &work_dir).unwrap(),
330 PathBuf::from("/workspace/data")
331 );
332 }
333
334 #[test]
335 fn test_convert_separators() {
336 let processor_wsl2 = PathProcessor::new(HostOs::WindowsWsl2, PathFormat::Wsl2);
337 assert_eq!(
338 processor_wsl2.convert_separators(r"C:\Users\test"),
339 "/mnt/c/Users/test"
340 );
341
342 let processor_windows = PathProcessor::new(HostOs::WindowsNative, PathFormat::Windows);
343 assert_eq!(
344 processor_windows.convert_separators("/mnt/c/Users/test"),
345 r"\mnt\c\Users\test"
346 );
347 }
348
349 #[test]
350 fn test_clean_path() {
351 let processor = PathProcessor::new(HostOs::LinuxNative, PathFormat::Posix);
352
353 assert_eq!(processor.clean_path("./data"), "data");
354 assert_eq!(processor.clean_path("data/./test"), "data/test");
355 assert_eq!(processor.clean_path("data/../test"), "test");
356 assert_eq!(processor.clean_path("./"), "");
357 }
358
359 #[test]
360 fn test_needs_special_handling() {
361 let processor_wsl2 = PathProcessor::new(HostOs::WindowsWsl2, PathFormat::Wsl2);
362 assert!(processor_wsl2.needs_special_handling("/mnt/c/data"));
363
364 let processor_posix = PathProcessor::new(HostOs::LinuxNative, PathFormat::Posix);
365 assert!(!processor_posix.needs_special_handling("/data"));
366 }
367}