Skip to main content

client_core/container/
path_utils.rs

1//! 路径处理工具
2//!
3//! 提供跨平台的路径处理功能,支持 WSL2、Windows 和 POSIX 路径格式。
4
5use crate::container::environment::{HostOs, PathFormat};
6use std::path::{Path, PathBuf};
7use tracing::debug;
8
9/// 路径处理错误
10#[derive(Debug, thiserror::Error)]
11pub enum PathUtilsError {
12    #[error("Path processing error: {0}")]
13    InvalidPath(String),
14}
15
16/// 跨平台路径处理器
17#[derive(Debug, Clone)]
18pub struct PathProcessor {
19    pub host_os: HostOs,
20    pub path_format: PathFormat,
21}
22
23impl PathProcessor {
24    /// 创建新的路径处理器
25    pub fn new(host_os: HostOs, path_format: PathFormat) -> Self {
26        Self {
27            host_os,
28            path_format,
29        }
30    }
31
32    /// 解析和规范化路径
33    /// 根据环境将输入路径转换为适合的格式
34    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(
39                "Path cannot be empty".to_string(),
40            ));
41        }
42
43        debug!(
44            "🔍 Normalizing path: '{}' (current environment: {:?})",
45            path, self.path_format
46        );
47
48        // 1. 初步清理路径
49        let cleaned_path = self.clean_path(path);
50
51        // 2. 根据环境转换路径格式
52        let formatted_path = match self.path_format {
53            PathFormat::Wsl2 => self.to_wsl2_format(&cleaned_path),
54            PathFormat::Windows => self.to_windows_format(&cleaned_path),
55            PathFormat::Posix => self.to_posix_format(&cleaned_path),
56        };
57
58        debug!("✅ Path normalization complete: '{}'", formatted_path);
59        Ok(formatted_path)
60    }
61
62    /// 检查是否为 bind mount 路径
63    pub fn is_bind_mount_path(&self, path: &str) -> bool {
64        let path = path.trim();
65
66        if path.is_empty() {
67            return false;
68        }
69
70        // 绝对路径(POSIX 格式)
71        if path.starts_with('/') && !path.starts_with("//") {
72            return true;
73        }
74
75        // Windows 绝对路径(C:\, D:\ 等)
76        if path.len() >= 3
77            && path.chars().nth(1).unwrap_or_default() == ':'
78            && (path.chars().nth(2).unwrap_or_default() == '\\'
79                || path.chars().nth(2).unwrap_or_default() == '/')
80        {
81            return true;
82        }
83
84        // WSL2 路径格式
85        if path.starts_with("/mnt/") || path.starts_with("/c/") || path.starts_with("/d/") {
86            return true;
87        }
88
89        // 相对路径(包含路径分隔符)
90        if path.contains('/') || path.contains('\\') {
91            return true;
92        }
93
94        false
95    }
96
97    /// 将路径转换为相对于工作目录的绝对路径
98    pub fn to_absolute_path(&self, path: &str, work_dir: &Path) -> Result<PathBuf, PathUtilsError> {
99        let normalized_path = self.normalize_path(path)?;
100        let path_buf = PathBuf::from(&normalized_path);
101
102        if path_buf.is_absolute() {
103            Ok(path_buf)
104        } else {
105            // 相对路径:相对于工作目录
106            let absolute = work_dir.join(path_buf);
107            Ok(absolute)
108        }
109    }
110
111    /// 清理路径(移除多余的 ./ 和 //)
112    fn clean_path(&self, path: &str) -> String {
113        let mut components: Vec<std::path::Component> = Vec::new();
114        let mut has_root = false;
115
116        for component in Path::new(path).components() {
117            match component {
118                std::path::Component::CurDir => {
119                    // 跳过当前目录 .
120                    continue;
121                }
122                std::path::Component::RootDir => {
123                    // 记录存在根目录,但不立即添加
124                    has_root = true;
125                }
126                std::path::Component::ParentDir => {
127                    // 处理父目录 ..
128                    if let Some(last) = components.last()
129                        && *last != std::path::Component::RootDir
130                    {
131                        components.pop();
132                    }
133                }
134                _ => {
135                    components.push(component);
136                }
137            }
138        }
139
140        let separator = std::path::MAIN_SEPARATOR_STR;
141        let cleaned = if has_root {
142            let prefix = if separator == "/" { "/" } else { "" };
143            format!(
144                "{}{}",
145                prefix,
146                components
147                    .iter()
148                    .map(|c| c.as_os_str().to_string_lossy())
149                    .collect::<Vec<_>>()
150                    .join(separator)
151            )
152        } else {
153            components
154                .iter()
155                .map(|c| c.as_os_str().to_string_lossy())
156                .collect::<Vec<_>>()
157                .join(separator)
158        };
159
160        // 确保空路径返回 "."
161        if cleaned.is_empty() {
162            ".".to_string()
163        } else {
164            cleaned
165        }
166    }
167
168    /// 转换为 WSL2 格式
169    fn to_wsl2_format(&self, path: &str) -> String {
170        let path = path.replace('\\', "/");
171
172        // 如果是 Windows 绝对路径(C:\...),转换为 WSL2 格式
173        if path.len() >= 3
174            && path.chars().nth(1).unwrap_or_default() == ':'
175            && (path.chars().nth(2).unwrap_or_default() == '/'
176                || path.chars().nth(2).unwrap_or_default() == '\\')
177        {
178            let drive_letter = path
179                .chars()
180                .nth(0)
181                .unwrap_or_default()
182                .to_lowercase()
183                .next()
184                .unwrap_or_default();
185            // path[2] is the separator '/', so path[2..] gives "/Users/test/data"
186            let rest = &path[2..];
187            return format!("/mnt/{}{}", drive_letter, rest);
188        }
189
190        // 如果已经是 WSL2 格式,返回
191        if path.starts_with("/mnt/") || path.starts_with("/c/") || path.starts_with("/d/") {
192            return path;
193        }
194
195        // 其他格式保持不变
196        path
197    }
198
199    /// 转换为 Windows 格式
200    fn to_windows_format(&self, path: &str) -> String {
201        // WSL2 路径转换为 Windows 路径(需要在替换斜杠之前处理)
202        if let Some(rest) = path
203            .strip_prefix("/mnt/")
204            .or_else(|| path.strip_prefix("\\mnt\\"))
205            && !rest.is_empty()
206        {
207            let drive_letter = rest
208                .chars()
209                .next()
210                .unwrap_or_default()
211                .to_uppercase()
212                .next()
213                .unwrap_or_default();
214            // 跳过驱动器字母后的分隔符(如 / 或 \),然后转换剩余路径的分隔符
215            let rest = rest[1..].trim_start_matches(['/', '\\']).replace('/', "\\");
216            return format!("{}:\\{}", drive_letter, rest);
217        }
218
219        if let Some(rest) = path
220            .strip_prefix("/c/")
221            .or_else(|| path.strip_prefix("\\c\\"))
222        {
223            let rest = rest.trim_start_matches(['/', '\\']).replace('/', "\\");
224            return format!("C:\\{}", rest);
225        }
226
227        if let Some(rest) = path
228            .strip_prefix("/d/")
229            .or_else(|| path.strip_prefix("\\d\\"))
230        {
231            let rest = rest.trim_start_matches(['/', '\\']).replace('/', "\\");
232            return format!("D:\\{}", rest);
233        }
234
235        // 其他情况:替换斜杠为反斜杠
236        path.replace('/', "\\")
237    }
238
239    /// 转换为 POSIX 格式
240    fn to_posix_format(&self, path: &str) -> String {
241        let path = path.replace('\\', "/");
242
243        // Windows 绝对路径转换为 POSIX
244        if path.len() >= 3
245            && path.chars().nth(1).unwrap_or_default() == ':'
246            && (path.chars().nth(2).unwrap_or_default() == '\\'
247                || path.chars().nth(2).unwrap_or_default() == '/')
248        {
249            let drive_letter = path
250                .chars()
251                .nth(0)
252                .unwrap_or_default()
253                .to_lowercase()
254                .next()
255                .unwrap_or_default();
256            let rest = &path[3..];
257            return format!("/mnt/{}{}", drive_letter, rest);
258        }
259
260        // WSL2 格式保持不变
261        if path.starts_with("/mnt/") || path.starts_with("/c/") || path.starts_with("/d/") {
262            return path;
263        }
264
265        path
266    }
267
268    /// 转换路径分隔符
269    pub fn convert_separators(&self, path: &str) -> String {
270        match self.path_format {
271            PathFormat::Wsl2 | PathFormat::Posix => path.replace('\\', "/"),
272            PathFormat::Windows => path.replace('/', "\\"),
273        }
274    }
275
276    /// 检查路径是否需要特殊处理
277    pub fn needs_special_handling(&self, path: &str) -> bool {
278        // Windows 路径或 WSL2 路径需要特殊处理
279        self.is_bind_mount_path(path)
280            && (self.path_format == PathFormat::Wsl2 || self.path_format == PathFormat::Windows)
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn test_normalize_path_wsl2() {
290        let processor = PathProcessor::new(HostOs::WindowsWsl2, PathFormat::Wsl2);
291
292        // Windows 路径转换为 WSL2
293        assert_eq!(
294            processor.normalize_path(r"C:\Users\test\data").unwrap(),
295            "/mnt/c/Users/test/data"
296        );
297
298        // 相对路径保持不变
299        assert_eq!(processor.normalize_path("./data").unwrap(), "data");
300
301        // 已经是 WSL2 格式
302        assert_eq!(
303            processor.normalize_path("/mnt/c/Users/test").unwrap(),
304            "/mnt/c/Users/test"
305        );
306    }
307
308    #[test]
309    fn test_normalize_path_windows() {
310        let processor = PathProcessor::new(HostOs::WindowsNative, PathFormat::Windows);
311
312        // WSL2 路径转换为 Windows
313        assert_eq!(
314            processor.normalize_path("/mnt/c/Users/test").unwrap(),
315            r"C:\Users\test"
316        );
317
318        // 路径分隔符转换
319        assert_eq!(
320            processor.normalize_path("C:/Users/test").unwrap(),
321            r"C:\Users\test"
322        );
323    }
324
325    #[test]
326    fn test_is_bind_mount_path_wsl2() {
327        let processor = PathProcessor::new(HostOs::WindowsWsl2, PathFormat::Wsl2);
328
329        assert!(processor.is_bind_mount_path("/mnt/c/Users/test/data"));
330        assert!(processor.is_bind_mount_path("/c/Users/test/data"));
331        assert!(processor.is_bind_mount_path("/data/mysql"));
332        assert!(processor.is_bind_mount_path("./data"));
333        assert!(processor.is_bind_mount_path("../data"));
334        assert!(processor.is_bind_mount_path(r"C:\data"));
335
336        // 这些不是 bind mount
337        assert!(!processor.is_bind_mount_path("volume_name"));
338        assert!(!processor.is_bind_mount_path(""));
339    }
340
341    #[test]
342    fn test_to_absolute_path() {
343        let processor = PathProcessor::new(HostOs::LinuxNative, PathFormat::Posix);
344        let work_dir = PathBuf::from("/workspace");
345
346        // 绝对路径保持不变
347        assert_eq!(
348            processor.to_absolute_path("/data", &work_dir).unwrap(),
349            PathBuf::from("/data")
350        );
351
352        // 相对路径相对于工作目录
353        assert_eq!(
354            processor.to_absolute_path("./data", &work_dir).unwrap(),
355            PathBuf::from("/workspace/data")
356        );
357    }
358
359    #[test]
360    fn test_convert_separators() {
361        // convert_separators just converts slashes, doesn't do path format conversion
362        let processor_wsl2 = PathProcessor::new(HostOs::WindowsWsl2, PathFormat::Wsl2);
363        assert_eq!(
364            processor_wsl2.convert_separators(r"C:\Users\test"),
365            "C:/Users/test"
366        );
367
368        let processor_windows = PathProcessor::new(HostOs::WindowsNative, PathFormat::Windows);
369        assert_eq!(
370            processor_windows.convert_separators("/mnt/c/Users/test"),
371            "\\mnt\\c\\Users\\test"
372        );
373    }
374
375    #[test]
376    fn test_clean_path() {
377        let processor = PathProcessor::new(HostOs::LinuxNative, PathFormat::Posix);
378
379        assert_eq!(processor.clean_path("./data"), "data");
380        assert_eq!(processor.clean_path("data/./test"), "data/test");
381        assert_eq!(processor.clean_path("data/../test"), "test");
382        // 空路径返回 "."
383        assert_eq!(processor.clean_path("./"), ".");
384    }
385
386    #[test]
387    fn test_needs_special_handling() {
388        let processor_wsl2 = PathProcessor::new(HostOs::WindowsWsl2, PathFormat::Wsl2);
389        assert!(processor_wsl2.needs_special_handling("/mnt/c/data"));
390
391        let processor_posix = PathProcessor::new(HostOs::LinuxNative, PathFormat::Posix);
392        assert!(!processor_posix.needs_special_handling("/data"));
393    }
394}