@@ -13,6 +13,70 @@ use std::sync::{
1313use tempfile:: TempDir ;
1414use tokio:: fs;
1515
16+ // Import platform-specific modules
17+ #[ cfg( unix) ]
18+ mod unix;
19+ #[ cfg( windows) ]
20+ mod windows;
21+
22+ // Use platform-specific functions
23+ #[ cfg( unix) ]
24+ use unix:: {
25+ append as platform_append,
26+ symlink_sync,
27+ } ;
28+ #[ cfg( windows) ]
29+ use windows:: {
30+ append as platform_append,
31+ symlink_sync,
32+ } ;
33+
34+ /// Rust path handling is hard coded to work specific ways depending on the
35+ /// OS that is being executed on. Because of this, if Unix paths are provided,
36+ /// they aren't recognized. For example a leading prefix of '/' isn't considered
37+ /// an absolute path. To fix this, all test paths would need to have windows
38+ /// equivalents which is tedious and can lead to errors and missed test cases.
39+ /// To make writing tests easier, path normalization happens on Windows systems
40+ /// implicitly during test runtime.
41+ #[ cfg( test) ]
42+ fn normalize_test_path ( path : impl AsRef < Path > ) -> PathBuf {
43+ #[ cfg( windows) ]
44+ {
45+ use typed_path:: Utf8TypedPath ;
46+ let path_ref = path. as_ref ( ) ;
47+
48+ // Only process string paths with forward slashes
49+ let typed_path = Utf8TypedPath :: derive ( path_ref. to_str ( ) . unwrap ( ) ) ;
50+ if typed_path. is_unix ( ) {
51+ let windows_path = typed_path. with_windows_encoding ( ) . to_string ( ) ;
52+
53+ // If path is absolute (starts with /) and doesn't already have a drive letter
54+ if PathBuf :: from ( & windows_path) . has_root ( ) {
55+ // Prepend C: drive letter to make it truly absolute on Windows
56+ return PathBuf :: from ( format ! ( "C:{}" , windows_path) ) ;
57+ }
58+
59+ return PathBuf :: from ( windows_path) ;
60+ }
61+ }
62+ path. as_ref ( ) . to_path_buf ( )
63+ }
64+
65+ /// Cross-platform path append that handles test paths consistently
66+ fn append ( base : impl AsRef < Path > , path : impl AsRef < Path > ) -> PathBuf {
67+ #[ cfg( test) ]
68+ {
69+ // Normalize the path for tests, then use the platform-specific append
70+ platform_append ( normalize_test_path ( base) , normalize_test_path ( path) )
71+ }
72+
73+ #[ cfg( not( test) ) ]
74+ {
75+ // In non-test code, just use the platform-specific append directly
76+ platform_append ( base, path)
77+ }
78+ }
79+
1680#[ derive( Debug , Clone , Default ) ]
1781pub struct Fs ( inner:: Inner ) ;
1882
@@ -285,29 +349,34 @@ impl Fs {
285349 /// Creates a new symbolic link on the filesystem.
286350 ///
287351 /// The `link` path will be a symbolic link pointing to the `original` path.
288- ///
289- /// This is a proxy to [`tokio::fs::symlink`].
290- #[ cfg( unix) ]
291352 pub async fn symlink ( & self , original : impl AsRef < Path > , link : impl AsRef < Path > ) -> io:: Result < ( ) > {
292353 use inner:: Inner ;
354+
355+ #[ cfg( unix) ]
356+ async fn do_symlink ( original : impl AsRef < Path > , link : impl AsRef < Path > ) -> io:: Result < ( ) > {
357+ fs:: symlink ( original, link) . await
358+ }
359+
360+ #[ cfg( windows) ]
361+ async fn do_symlink ( original : impl AsRef < Path > , link : impl AsRef < Path > ) -> io:: Result < ( ) > {
362+ windows:: symlink_async ( original, link) . await
363+ }
364+
293365 match & self . 0 {
294- Inner :: Real => fs :: symlink ( original, link) . await ,
295- Inner :: Chroot ( root) => fs :: symlink ( append ( root. path ( ) , original) , append ( root. path ( ) , link) ) . await ,
366+ Inner :: Real => do_symlink ( original, link) . await ,
367+ Inner :: Chroot ( root) => do_symlink ( append ( root. path ( ) , original) , append ( root. path ( ) , link) ) . await ,
296368 Inner :: Fake ( _) => panic ! ( "unimplemented" ) ,
297369 }
298370 }
299371
300372 /// Creates a new symbolic link on the filesystem.
301373 ///
302374 /// The `link` path will be a symbolic link pointing to the `original` path.
303- ///
304- /// This is a proxy to [`std::os::unix::fs::symlink`].
305- #[ cfg( unix) ]
306375 pub fn symlink_sync ( & self , original : impl AsRef < Path > , link : impl AsRef < Path > ) -> io:: Result < ( ) > {
307376 use inner:: Inner ;
308377 match & self . 0 {
309- Inner :: Real => std :: os :: unix :: fs :: symlink ( original, link) ,
310- Inner :: Chroot ( root) => std :: os :: unix :: fs :: symlink ( append ( root. path ( ) , original) , append ( root. path ( ) , link) ) ,
378+ Inner :: Real => symlink_sync ( original, link) ,
379+ Inner :: Chroot ( root) => symlink_sync ( append ( root. path ( ) , original) , append ( root. path ( ) , link) ) ,
311380 Inner :: Fake ( _) => panic ! ( "unimplemented" ) ,
312381 }
313382 }
@@ -403,35 +472,6 @@ impl Fs {
403472 }
404473}
405474
406- /// Performs `a.join(b)`, except:
407- /// - if `b` is an absolute path, then the resulting path will equal `/a/b`
408- /// - if the prefix of `b` contains some `n` copies of a, then the resulting path will equal `/a/b`
409- #[ cfg( unix) ]
410- fn append ( a : impl AsRef < Path > , b : impl AsRef < Path > ) -> PathBuf {
411- use std:: ffi:: OsString ;
412- use std:: os:: unix:: ffi:: {
413- OsStrExt ,
414- OsStringExt ,
415- } ;
416-
417- // Have to use byte slices since rust seems to always append
418- // a forward slash at the end of a path...
419- let a = a. as_ref ( ) . as_os_str ( ) . as_bytes ( ) ;
420- let mut b = b. as_ref ( ) . as_os_str ( ) . as_bytes ( ) ;
421- while b. starts_with ( a) {
422- b = b. strip_prefix ( a) . unwrap ( ) ;
423- }
424- while b. starts_with ( b"/" ) {
425- b = b. strip_prefix ( b"/" ) . unwrap ( ) ;
426- }
427- PathBuf :: from ( OsString :: from_vec ( a. to_vec ( ) ) ) . join ( PathBuf :: from ( OsString :: from_vec ( b. to_vec ( ) ) ) )
428- }
429-
430- #[ cfg( windows) ]
431- fn append ( a : impl AsRef < Path > , b : impl AsRef < Path > ) -> PathBuf {
432- todo ! ( )
433- }
434-
435475#[ cfg( test) ]
436476mod tests {
437477 use super :: * ;
@@ -466,18 +506,25 @@ mod tests {
466506 assert_eq ! ( fs. read_to_string( dir. path( ) . join( "write" ) ) . await . unwrap( ) , "write" ) ;
467507 }
468508
469- #[ test]
470- fn test_append ( ) {
471- macro_rules! assert_append {
472- ( $a: expr, $b: expr, $expected: expr) => {
473- assert_eq!( append( $a, $b) , PathBuf :: from( $expected) ) ;
474- } ;
475- }
476- assert_append ! ( "/abc/test" , "/test" , "/abc/test/test" ) ;
477- assert_append ! ( "/tmp/.dir" , "/tmp/.dir/home/myuser" , "/tmp/.dir/home/myuser" ) ;
478- assert_append ! ( "/tmp/.dir" , "/tmp/hello" , "/tmp/.dir/tmp/hello" ) ;
479- assert_append ! ( "/tmp/.dir" , "/tmp/.dir/tmp/.dir/home/user" , "/tmp/.dir/home/user" ) ;
480- }
509+ macro_rules! test_append_cases {
510+ ( $(
511+ $name: ident: ( $a: expr, $b: expr) => $expected: expr
512+ ) ,* $( , ) ?) => {
513+ $(
514+ #[ test]
515+ fn $name( ) {
516+ assert_eq!( append( $a, $b) , normalize_test_path( $expected) ) ;
517+ }
518+ ) *
519+ } ;
520+ }
521+
522+ test_append_cases ! (
523+ append_test_path_to_dir: ( "/abc/test" , "/test" ) => "/abc/test/test" ,
524+ append_absolute_to_tmp_dir: ( "/tmp/.dir" , "/tmp/.dir/home/myuser" ) => "/tmp/.dir/home/myuser" ,
525+ append_different_tmp_path: ( "/tmp/.dir" , "/tmp/hello" ) => "/tmp/.dir/tmp/hello" ,
526+ append_nested_path_to_tmpdir: ( "/tmp/.dir" , "/tmp/.dir/tmp/.dir/home/user" ) => "/tmp/.dir/home/user" ,
527+ ) ;
481528
482529 #[ tokio:: test]
483530 async fn test_read_to_string ( ) {
0 commit comments