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("Path cannot be empty".to_string()));
39        }
40
41        debug!(
42            "🔍 Normalizing path: '{}' (current environment: {:?})",
43            path, self.path_format
44        );
45
46        // 1. 初步清理路径
47        let cleaned_path = self.clean_path(path);
48
49        // 2. 根据环境转换路径格式
50        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    /// 检查是否为 bind mount 路径
61    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        // 绝对路径(POSIX 格式)
69        if path.starts_with('/') && !path.starts_with("//") {
70            return true;
71        }
72
73        // Windows 绝对路径(C:\, D:\ 等)
74        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        // WSL2 路径格式
83        if path.starts_with("/mnt/") || path.starts_with("/c/") || path.starts_with("/d/") {
84            return true;
85        }
86
87        // 相对路径(包含路径分隔符)
88        if path.contains('/') || path.contains('\\') {
89            // 排除仅包含文件名的情况
90            return !Path::new(path).file_name().map_or(false, |f| {
91                !Path::new(f).extension().map_or(false, |ext| {
92                    // 排除带扩展名的文件
93                    ext.len() > 0
94                })
95            });
96        }
97
98        false
99    }
100
101    /// 将路径转换为相对于工作目录的绝对路径
102    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            // 相对路径:相对于工作目录
110            let absolute = work_dir.join(path_buf);
111            Ok(absolute)
112        }
113    }
114
115    /// 清理路径(移除多余的 ./ 和 //)
116    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                    // 跳过当前目录 .
123                    continue;
124                }
125                std::path::Component::ParentDir => {
126                    // 处理父目录 ..
127                    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        // 确保空路径返回 "."
146        if cleaned.is_empty() {
147            ".".to_string()
148        } else {
149            cleaned
150        }
151    }
152
153    /// 转换为 WSL2 格式
154    fn to_wsl2_format(&self, path: &str) -> String {
155        let path = path.replace('\\', "/");
156
157        // 如果是 Windows 绝对路径(C:\...),转换为 WSL2 格式
158        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        // 如果已经是 WSL2 格式,返回
175        if path.starts_with("/mnt/") || path.starts_with("/c/") || path.starts_with("/d/") {
176            return path;
177        }
178
179        // 其他格式保持不变
180        path
181    }
182
183    /// 转换为 Windows 格式
184    fn to_windows_format(&self, path: &str) -> String {
185        let path = path.replace('/', "\\");
186
187        // WSL2 路径转换为 Windows 路径
188        if path.starts_with("/mnt/") {
189            let rest = &path[5..]; // 移除 /mnt/
190            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    /// 转换为 POSIX 格式
215    fn to_posix_format(&self, path: &str) -> String {
216        let path = path.replace('\\', "/");
217
218        // Windows 绝对路径转换为 POSIX
219        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        // WSL2 格式保持不变
236        if path.starts_with("/mnt/") || path.starts_with("/c/") || path.starts_with("/d/") {
237            return path;
238        }
239
240        path
241    }
242
243    /// 转换路径分隔符
244    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    /// 检查路径是否需要特殊处理
252    pub fn needs_special_handling(&self, path: &str) -> bool {
253        // Windows 路径或 WSL2 路径需要特殊处理
254        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        // Windows 路径转换为 WSL2
268        assert_eq!(
269            processor.normalize_path(r"C:\Users\test\data").unwrap(),
270            "/mnt/c/Users/test/data"
271        );
272
273        // 相对路径保持不变
274        assert_eq!(processor.normalize_path("./data").unwrap(), "data");
275
276        // 已经是 WSL2 格式
277        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        // WSL2 路径转换为 Windows
288        assert_eq!(
289            processor.normalize_path("/mnt/c/Users/test").unwrap(),
290            r"C:\Users\test"
291        );
292
293        // 路径分隔符转换
294        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        // 这些不是 bind mount
312        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        // 绝对路径保持不变
322        assert_eq!(
323            processor.to_absolute_path("/data", &work_dir).unwrap(),
324            PathBuf::from("/data")
325        );
326
327        // 相对路径相对于工作目录
328        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}