rusty_stack/platform/
path_bridge.rs1use serde::{Deserialize, Serialize};
23
24pub const DEFAULT_WSL_DISTRO: &str = "Ubuntu";
26
27pub fn windows_to_wsl(path: &str) -> String {
50 let path = path.trim();
51
52 if let Some(rest) = try_strip_unc_wsl_prefix(path) {
54 let posix = normalize_to_posix(&rest);
56 if posix.is_empty() {
57 return "/".to_string();
58 }
59 if posix.starts_with('/') {
60 return posix;
61 }
62 return format!("/{}", posix);
63 }
64
65 if let Some(result) = try_drive_letter_to_wsl(path) {
67 return result;
68 }
69
70 normalize_to_posix(path)
72}
73
74pub fn wsl_to_windows(path: &str) -> String {
90 wsl_to_windows_with_distro(path, DEFAULT_WSL_DISTRO)
91}
92
93pub fn wsl_to_windows_with_distro(path: &str, distro: &str) -> String {
97 let path = path.trim();
98
99 if let Some(result) = try_mnt_to_windows(path) {
101 return result;
102 }
103
104 format!("\\\\wsl$\\{}{}", distro, normalize_to_windows(path))
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub struct PathTranslation {
111 pub translated: String,
113 pub direction: TranslationDirection,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
119pub enum TranslationDirection {
120 WindowsToWsl,
122 WslToWindows,
124}
125
126pub fn translate_path(path: &str) -> PathTranslation {
132 let path = path.trim();
133
134 if is_windows_path(path) {
135 PathTranslation {
136 translated: windows_to_wsl(path),
137 direction: TranslationDirection::WindowsToWsl,
138 }
139 } else {
140 PathTranslation {
141 translated: wsl_to_windows(path),
142 direction: TranslationDirection::WslToWindows,
143 }
144 }
145}
146
147pub fn is_windows_path(path: &str) -> bool {
153 let path = path.trim();
154
155 if path.len() >= 3 {
157 let bytes = path.as_bytes();
158 if bytes[0].is_ascii_alphabetic()
159 && bytes[1] == b':'
160 && (bytes[2] == b'\\' || bytes[2] == b'/')
161 {
162 return true;
163 }
164 }
165
166 path.starts_with(r"\\") || path.starts_with("//")
168}
169
170fn try_strip_unc_wsl_prefix(path: &str) -> Option<String> {
179 let normalized = path.replace('/', r"\");
181
182 if let Some(rest) = normalized.to_lowercase().strip_prefix(r"\\wsl$\") {
184 if let Some(slash_pos) = rest.find('\\') {
186 let distro = &rest[..slash_pos];
187 let remaining = &normalized[r"\\wsl$\".len() + distro.len()..];
188 return Some(remaining.to_string());
189 }
190 return Some(String::new());
192 }
193
194 if let Some(rest) = normalized.to_lowercase().strip_prefix(r"\\wsl.localhost\") {
196 if let Some(slash_pos) = rest.find('\\') {
197 let distro = &rest[..slash_pos];
198 let remaining = &normalized[r"\\wsl.localhost\".len() + distro.len()..];
199 return Some(remaining.to_string());
200 }
201 return Some(String::new());
202 }
203
204 None
205}
206
207fn try_drive_letter_to_wsl(path: &str) -> Option<String> {
212 let path = path.trim();
213
214 if path.len() < 2 {
215 return None;
216 }
217
218 let bytes = path.as_bytes();
219 if !bytes[0].is_ascii_alphabetic() || bytes[1] != b':' {
220 return None;
221 }
222
223 let drive = bytes[0].to_ascii_lowercase();
224
225 if path.len() == 2 {
226 return Some(format!("/mnt/{}", drive as char));
228 }
229
230 let rest = &path[2..]; let rest = normalize_to_posix(rest);
232
233 Some(format!("/mnt/{}{}", drive as char, rest))
234}
235
236fn try_mnt_to_windows(path: &str) -> Option<String> {
240 let path = path.trim();
241
242 if !path.starts_with("/mnt/") {
243 return None;
244 }
245
246 let rest = &path[5..]; let drive = rest.chars().next()?;
250 if !drive.is_ascii_alphabetic() {
251 return None;
252 }
253
254 let after_drive = &rest[drive.len_utf8()..];
255
256 if after_drive.is_empty() {
258 return Some(format!("{}:\\", drive.to_ascii_uppercase()));
259 }
260
261 if !after_drive.starts_with('/') {
262 return None;
263 }
264
265 let windows_rest = normalize_to_windows(after_drive);
266 Some(format!("{}:{}", drive.to_ascii_uppercase(), windows_rest))
267}
268
269fn normalize_to_posix(path: &str) -> String {
271 let result = path.replace('\\', "/");
272
273 let mut collapsed = String::with_capacity(result.len());
275 let mut last_was_slash = false;
276 for ch in result.chars() {
277 if ch == '/' {
278 if !last_was_slash {
279 collapsed.push('/');
280 }
281 last_was_slash = true;
282 } else {
283 collapsed.push(ch);
284 last_was_slash = false;
285 }
286 }
287
288 collapsed
289}
290
291fn normalize_to_windows(path: &str) -> String {
293 path.replace('/', r"\")
294}
295
296#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
309 fn test_windows_to_wsl_basic_drive() {
310 assert_eq!(
311 windows_to_wsl(r"C:\Users\alice\mlstack"),
312 "/mnt/c/Users/alice/mlstack"
313 );
314 }
315
316 #[test]
317 fn test_windows_to_wsl_drive_d() {
318 assert_eq!(windows_to_wsl(r"D:\data\file.txt"), "/mnt/d/data/file.txt");
319 }
320
321 #[test]
322 fn test_windows_to_wsl_root_drive() {
323 assert_eq!(windows_to_wsl(r"C:\"), "/mnt/c/");
324 }
325
326 #[test]
327 fn test_windows_to_wsl_drive_only() {
328 assert_eq!(windows_to_wsl(r"C:"), "/mnt/c");
329 }
330
331 #[test]
332 fn test_windows_to_wsl_forward_slashes() {
333 assert_eq!(
335 windows_to_wsl("C:/Users/alice/mlstack"),
336 "/mnt/c/Users/alice/mlstack"
337 );
338 }
339
340 #[test]
341 fn test_windows_to_wsl_unc_wsl_path() {
342 assert_eq!(
343 windows_to_wsl(r"\\wsl$\Ubuntu\home\alice\.mlstack"),
344 "/home/alice/.mlstack"
345 );
346 }
347
348 #[test]
349 fn test_windows_to_wsl_unc_wsl_path_case_insensitive() {
350 let result = windows_to_wsl(r"\\WSL$\ubuntu\home\test");
352 assert!(result.contains("home"), "Should extract path after distro");
353 }
354
355 #[test]
356 fn test_windows_to_wsl_unc_wsl_localhost() {
357 let result = windows_to_wsl(r"\\wsl.localhost\Ubuntu\home\alice");
358 assert!(
359 result.contains("home"),
360 "Should handle wsl.localhost UNC prefix"
361 );
362 }
363
364 #[test]
365 fn test_windows_to_wsl_unc_wsl_distro_only() {
366 let result = windows_to_wsl(r"\\wsl$\Ubuntu");
368 assert_eq!(result, "/");
369 }
370
371 #[test]
376 fn test_wsl_to_windows_mnt_path() {
377 assert_eq!(wsl_to_windows("/mnt/c/Users/alice"), r"C:\Users\alice");
378 }
379
380 #[test]
381 fn test_wsl_to_windows_mnt_d() {
382 assert_eq!(wsl_to_windows("/mnt/d/data/file.txt"), r"D:\data\file.txt");
383 }
384
385 #[test]
386 fn test_wsl_to_windows_home_path() {
387 assert_eq!(
388 wsl_to_windows("/home/alice/.mlstack"),
389 r"\\wsl$\Ubuntu\home\alice\.mlstack"
390 );
391 }
392
393 #[test]
394 fn test_wsl_to_windows_custom_distro() {
395 assert_eq!(
396 wsl_to_windows_with_distro("/home/alice/.mlstack", "Debian"),
397 r"\\wsl$\Debian\home\alice\.mlstack"
398 );
399 }
400
401 #[test]
402 fn test_wsl_to_windows_mnt_root() {
403 assert_eq!(wsl_to_windows("/mnt/c/"), r"C:\");
404 }
405
406 #[test]
407 fn test_wsl_to_windows_mnt_drive_only() {
408 assert_eq!(wsl_to_windows("/mnt/c"), r"C:\");
409 }
410
411 #[test]
416 fn test_windows_to_wsl_spaces() {
417 assert_eq!(
418 windows_to_wsl(r"C:\Program Files\ML Stack"),
419 "/mnt/c/Program Files/ML Stack"
420 );
421 }
422
423 #[test]
424 fn test_wsl_to_windows_spaces() {
425 assert_eq!(
426 wsl_to_windows("/mnt/c/Program Files/ML Stack"),
427 r"C:\Program Files\ML Stack"
428 );
429 }
430
431 #[test]
432 fn test_windows_to_wsl_unicode() {
433 assert_eq!(
434 windows_to_wsl(r"C:\Users\日本語\ドキュメント"),
435 "/mnt/c/Users/日本語/ドキュメント"
436 );
437 }
438
439 #[test]
440 fn test_wsl_to_windows_unicode() {
441 assert_eq!(
442 wsl_to_windows("/mnt/c/Users/日本語/ドキュメント"),
443 r"C:\Users\日本語\ドキュメント"
444 );
445 }
446
447 #[test]
448 fn test_unicode_roundtrip_windows_to_wsl_and_back() {
449 let original = r"C:\Users\alice\文档\ml-stack";
450 let wsl = windows_to_wsl(original);
451 let back = wsl_to_windows(&wsl);
452 assert_eq!(original, &back);
453 }
454
455 #[test]
456 fn test_spaces_roundtrip_windows_to_wsl_and_back() {
457 let original = r"C:\Users\My User\ML Stack Data";
458 let wsl = windows_to_wsl(original);
459 let back = wsl_to_windows(&wsl);
460 assert_eq!(original, &back);
461 }
462
463 #[test]
464 fn test_unicode_home_to_windows() {
465 let path = "/home/alice/文档/数据";
466 let win = wsl_to_windows(path);
467 assert_eq!(win, r"\\wsl$\Ubuntu\home\alice\文档\数据");
468 }
469
470 #[test]
471 fn test_unicode_home_roundtrip() {
472 let original = "/home/alice/文档/数据";
473 let win = wsl_to_windows(original);
474 assert_eq!(win, r"\\wsl$\Ubuntu\home\alice\文档\数据");
475 let back = windows_to_wsl(&win);
476 assert_eq!(back, "/home/alice/文档/数据");
478 }
479
480 #[test]
485 fn test_is_windows_path_drive_letter() {
486 assert!(is_windows_path(r"C:\Users"));
487 assert!(is_windows_path("D:/data"));
488 }
489
490 #[test]
491 fn test_is_windows_path_unc() {
492 assert!(is_windows_path(r"\\wsl$\Ubuntu\home"));
493 assert!(is_windows_path("//server/share"));
494 }
495
496 #[test]
497 fn test_is_not_windows_path() {
498 assert!(!is_windows_path("/home/alice"));
499 assert!(!is_windows_path("/mnt/c/Users"));
500 assert!(!is_windows_path("relative/path"));
501 }
502
503 #[test]
508 fn test_translate_path_auto_detect_windows() {
509 let result = translate_path(r"C:\Users\alice");
510 assert_eq!(result.direction, TranslationDirection::WindowsToWsl);
511 assert_eq!(result.translated, "/mnt/c/Users/alice");
512 }
513
514 #[test]
515 fn test_translate_path_auto_detect_linux() {
516 let result = translate_path("/home/alice");
517 assert_eq!(result.direction, TranslationDirection::WslToWindows);
518 assert!(result.translated.contains("alice"));
519 }
520
521 #[test]
526 fn test_path_translation_serde_roundtrip() {
527 let t = PathTranslation {
528 translated: "/mnt/c/Users".to_string(),
529 direction: TranslationDirection::WindowsToWsl,
530 };
531 let json = serde_json::to_string(&t).unwrap();
532 let back: PathTranslation = serde_json::from_str(&json).unwrap();
533 assert_eq!(t, back);
534 }
535
536 #[test]
537 fn test_translation_direction_serde_roundtrip() {
538 for dir in [
539 TranslationDirection::WindowsToWsl,
540 TranslationDirection::WslToWindows,
541 ] {
542 let json = serde_json::to_string(&dir).unwrap();
543 let back: TranslationDirection = serde_json::from_str(&json).unwrap();
544 assert_eq!(dir, back);
545 }
546 }
547
548 #[test]
553 fn test_empty_path() {
554 assert_eq!(windows_to_wsl(""), "");
555 assert_eq!(wsl_to_windows(""), r"\\wsl$\Ubuntu");
556 }
557
558 #[test]
559 fn test_whitespace_trimming() {
560 assert_eq!(windows_to_wsl(r" C:\Users\alice "), "/mnt/c/Users/alice");
561 assert_eq!(
562 wsl_to_windows(" /home/alice "),
563 r"\\wsl$\Ubuntu\home\alice"
564 );
565 }
566
567 #[test]
568 fn test_multiple_drives() {
569 assert_eq!(windows_to_wsl(r"E:\test"), "/mnt/e/test");
570 assert_eq!(windows_to_wsl(r"Z:\foo\bar"), "/mnt/z/foo/bar");
571 }
572
573 #[test]
574 fn test_mnt_uppercase_drive() {
575 let result = wsl_to_windows("/mnt/C/Users");
578 assert_eq!(result, r"C:\Users");
579 }
580
581 #[test]
582 fn test_deeply_nested_path() {
583 let path = r"C:\Users\alice\Documents\Projects\ML Stack\models\v1\checkpoint.bin";
584 let wsl = windows_to_wsl(path);
585 assert_eq!(
586 wsl,
587 "/mnt/c/Users/alice/Documents/Projects/ML Stack/models/v1/checkpoint.bin"
588 );
589 let back = wsl_to_windows(&wsl);
590 assert_eq!(path, &back);
591 }
592
593 #[test]
594 fn test_trailing_slash_handling() {
595 assert_eq!(windows_to_wsl(r"C:\Users\"), "/mnt/c/Users/");
596 assert_eq!(wsl_to_windows("/mnt/c/"), r"C:\");
597 }
598}