cyril_core/platform/
path.rs1use std::path::{Path, PathBuf};
2
3use serde_json::Value;
4
5pub fn to_native(path: &Path) -> PathBuf {
9 if cfg!(target_os = "windows") {
10 wsl_to_win(&path.to_string_lossy())
11 } else {
12 path.to_path_buf()
13 }
14}
15
16pub fn to_agent(path: &Path) -> PathBuf {
20 if cfg!(target_os = "windows") {
21 win_to_wsl(path)
22 } else {
23 path.to_path_buf()
24 }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum Direction {
30 WinToWsl,
31 WslToWin,
32}
33
34pub fn win_to_wsl(path: &Path) -> PathBuf {
40 let s = path.to_string_lossy();
41 let s = s.strip_prefix(r"\\?\").unwrap_or(&s);
43 if s.len() >= 2 && s.as_bytes()[1] == b':' {
45 let drive = s.as_bytes()[0].to_ascii_lowercase() as char;
46 let rest = &s[2..];
47 let rest = rest.replace('\\', "/");
48 let rest = rest.trim_start_matches('/');
49 if rest.is_empty() {
50 PathBuf::from(format!("/mnt/{drive}"))
51 } else {
52 PathBuf::from(format!("/mnt/{drive}/{rest}"))
53 }
54 } else {
55 PathBuf::from(s.replace('\\', "/"))
57 }
58}
59
60pub fn wsl_to_win(path: &str) -> PathBuf {
65 if let Some(rest) = path.strip_prefix("/mnt/") {
66 if rest.len() >= 1 {
67 let drive = rest.as_bytes()[0].to_ascii_uppercase() as char;
68 let after_drive = &rest[1..];
69 if after_drive.is_empty() || after_drive.starts_with('/') {
70 let suffix = after_drive.strip_prefix('/').unwrap_or("");
71 let win_path = if suffix.is_empty() {
72 format!("{drive}:\\")
73 } else {
74 format!("{drive}:\\{}", suffix.replace('/', "\\"))
75 };
76 return PathBuf::from(win_path);
77 }
78 }
79 }
80 PathBuf::from(path)
82}
83
84pub fn translate_paths_in_json(value: &mut Value, direction: Direction) {
87 match value {
88 Value::String(s) => {
89 let translated = match direction {
90 Direction::WinToWsl => {
91 if looks_like_windows_path(s) {
92 win_to_wsl(Path::new(s.as_str()))
93 .to_string_lossy()
94 .into_owned()
95 } else {
96 return;
97 }
98 }
99 Direction::WslToWin => {
100 if looks_like_wsl_mount_path(s) {
101 wsl_to_win(s).to_string_lossy().into_owned()
102 } else {
103 return;
104 }
105 }
106 };
107 *s = translated;
108 }
109 Value::Array(arr) => {
110 for item in arr {
111 translate_paths_in_json(item, direction);
112 }
113 }
114 Value::Object(map) => {
115 for (_, v) in map.iter_mut() {
116 translate_paths_in_json(v, direction);
117 }
118 }
119 _ => {}
120 }
121}
122
123fn looks_like_windows_path(s: &str) -> bool {
124 let s = s.strip_prefix(r"\\?\").unwrap_or(s);
126 s.len() >= 3
127 && s.as_bytes()[0].is_ascii_alphabetic()
128 && s.as_bytes()[1] == b':'
129 && (s.as_bytes()[2] == b'\\' || s.as_bytes()[2] == b'/')
130}
131
132fn looks_like_wsl_mount_path(s: &str) -> bool {
133 if let Some(rest) = s.strip_prefix("/mnt/") {
134 rest.len() >= 1 && rest.as_bytes()[0].is_ascii_alphabetic()
135 } else {
136 false
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn test_win_to_wsl_c_drive() {
146 assert_eq!(
147 win_to_wsl(Path::new(r"C:\Users\foo\bar")),
148 PathBuf::from("/mnt/c/Users/foo/bar")
149 );
150 }
151
152 #[test]
153 fn test_win_to_wsl_d_drive() {
154 assert_eq!(
155 win_to_wsl(Path::new(r"D:\project\src")),
156 PathBuf::from("/mnt/d/project/src")
157 );
158 }
159
160 #[test]
161 fn test_win_to_wsl_root() {
162 assert_eq!(
163 win_to_wsl(Path::new(r"C:\")),
164 PathBuf::from("/mnt/c")
165 );
166 }
167
168 #[test]
169 fn test_win_to_wsl_forward_slashes() {
170 assert_eq!(
171 win_to_wsl(Path::new("C:/Users/foo")),
172 PathBuf::from("/mnt/c/Users/foo")
173 );
174 }
175
176 #[test]
177 fn test_wsl_to_win_basic() {
178 assert_eq!(
179 wsl_to_win("/mnt/c/Users/foo/bar"),
180 PathBuf::from(r"C:\Users\foo\bar")
181 );
182 }
183
184 #[test]
185 fn test_wsl_to_win_d_drive() {
186 assert_eq!(
187 wsl_to_win("/mnt/d/project"),
188 PathBuf::from(r"D:\project")
189 );
190 }
191
192 #[test]
193 fn test_wsl_to_win_root() {
194 assert_eq!(
195 wsl_to_win("/mnt/c"),
196 PathBuf::from(r"C:\")
197 );
198 }
199
200 #[test]
201 fn test_wsl_to_win_non_mount_path() {
202 assert_eq!(
203 wsl_to_win("/home/user/.config"),
204 PathBuf::from("/home/user/.config")
205 );
206 }
207
208 #[test]
209 fn test_roundtrip_win_wsl_win() {
210 let original = r"C:\Users\dwall\repos\project\src\main.rs";
211 let wsl = win_to_wsl(Path::new(original));
212 let back = wsl_to_win(&wsl.to_string_lossy());
213 assert_eq!(back, PathBuf::from(original));
214 }
215
216 #[test]
217 fn test_translate_json_wsl_to_win() {
218 let mut val = serde_json::json!({
219 "path": "/mnt/c/Users/foo/file.txt",
220 "content": "hello world",
221 "nested": {
222 "file": "/mnt/d/project/src/main.rs"
223 }
224 });
225 translate_paths_in_json(&mut val, Direction::WslToWin);
226 assert_eq!(val["path"], r"C:\Users\foo\file.txt");
227 assert_eq!(val["content"], "hello world");
228 assert_eq!(val["nested"]["file"], r"D:\project\src\main.rs");
229 }
230
231 #[test]
232 fn test_translate_json_win_to_wsl() {
233 let mut val = serde_json::json!({
234 "path": r"C:\Users\foo\file.txt",
235 "count": 42
236 });
237 translate_paths_in_json(&mut val, Direction::WinToWsl);
238 assert_eq!(val["path"], "/mnt/c/Users/foo/file.txt");
239 assert_eq!(val["count"], 42);
240 }
241
242 #[test]
245 fn test_win_to_wsl_strips_extended_prefix() {
246 assert_eq!(
247 win_to_wsl(Path::new(r"\\?\C:\Users\foo\bar")),
248 PathBuf::from("/mnt/c/Users/foo/bar")
249 );
250 }
251
252 #[test]
253 fn test_win_to_wsl_strips_extended_prefix_d_drive() {
254 assert_eq!(
255 win_to_wsl(Path::new(r"\\?\D:\project\src")),
256 PathBuf::from("/mnt/d/project/src")
257 );
258 }
259
260 #[test]
261 fn test_win_to_wsl_extended_prefix_root() {
262 assert_eq!(
263 win_to_wsl(Path::new(r"\\?\C:\")),
264 PathBuf::from("/mnt/c")
265 );
266 }
267
268 #[test]
269 fn test_roundtrip_extended_prefix() {
270 let original = r"\\?\C:\Users\dwall\repos\project\src\main.rs";
271 let wsl = win_to_wsl(Path::new(original));
272 assert_eq!(wsl, PathBuf::from("/mnt/c/Users/dwall/repos/project/src/main.rs"));
273 let back = wsl_to_win(&wsl.to_string_lossy());
274 assert_eq!(back, PathBuf::from(r"C:\Users\dwall\repos\project\src\main.rs"));
276 }
277
278 #[test]
279 fn test_translate_json_extended_prefix() {
280 let mut val = serde_json::json!({
281 "path": r"\\?\C:\Users\foo\file.txt",
282 "normal": r"D:\project\src\main.rs"
283 });
284 translate_paths_in_json(&mut val, Direction::WinToWsl);
285 assert_eq!(val["path"], "/mnt/c/Users/foo/file.txt");
286 assert_eq!(val["normal"], "/mnt/d/project/src/main.rs");
287 }
288
289 #[test]
290 fn test_unc_path_not_mangled() {
291 let result = win_to_wsl(Path::new(r"\\server\share\file.txt"));
293 assert_eq!(result, PathBuf::from("//server/share/file.txt"));
294 }
295
296 #[test]
297 fn test_translate_json_unc_path_not_translated() {
298 let mut val = serde_json::json!({
299 "path": r"\\?\UNC\server\share\file.txt"
300 });
301 translate_paths_in_json(&mut val, Direction::WinToWsl);
302 assert_eq!(val["path"], r"\\?\UNC\server\share\file.txt");
305 }
306}