11use std:: collections:: HashMap ;
22use std:: fs:: Permissions ;
33use std:: io;
4+ #[ cfg( unix) ]
45use std:: os:: unix:: ffi:: OsStrExt ;
5- use std:: path:: {
6- Path ,
7- PathBuf ,
8- } ;
9- use std:: sync:: {
10- Arc ,
11- Mutex ,
12- } ;
6+ use std:: path:: { Path , PathBuf } ;
7+ use std:: sync:: { Arc , Mutex } ;
138
149use tempfile:: TempDir ;
1510use tokio:: fs;
@@ -22,10 +17,7 @@ pub struct Fs(inner::Inner);
2217mod inner {
2318 use std:: collections:: HashMap ;
2419 use std:: path:: PathBuf ;
25- use std:: sync:: {
26- Arc ,
27- Mutex ,
28- } ;
20+ use std:: sync:: { Arc , Mutex } ;
2921
3022 use tempfile:: TempDir ;
3123
@@ -304,6 +296,52 @@ impl Fs {
304296 }
305297 }
306298
299+ /// Creates a new symbolic link on the filesystem.
300+ ///
301+ /// The `link` path will be a symbolic link pointing to the `original` path.
302+ ///
303+ /// This function works for both files and directories on all platforms.
304+ /// On Windows, it automatically detects whether the target is a file or directory
305+ /// and uses the appropriate system call.
306+ ///
307+ /// This is a proxy to [`tokio::fs::symlink_file`] or [`tokio::fs::symlink_dir`] on Windows,
308+ /// and [`tokio::fs::symlink`] on Unix.
309+ #[ cfg( windows) ]
310+ pub async fn symlink ( & self , original : impl AsRef < Path > , link : impl AsRef < Path > ) -> io:: Result < ( ) > {
311+ use inner:: Inner ;
312+
313+ let original_path = original. as_ref ( ) ;
314+
315+ // Check if the original path exists and is a directory
316+ let is_dir = if let Ok ( metadata) = std:: fs:: metadata ( original_path) {
317+ metadata. is_dir ( )
318+ } else {
319+ // If the path doesn't exist, check if it ends with a path separator
320+ // This is a heuristic and not foolproof
321+ original_path. to_string_lossy ( ) . ends_with ( [ '/' , '\\' ] )
322+ } ;
323+
324+ match & self . 0 {
325+ Inner :: Real => {
326+ if is_dir {
327+ fs:: symlink_dir ( original_path, link) . await
328+ } else {
329+ fs:: symlink_file ( original_path, link) . await
330+ }
331+ } ,
332+ Inner :: Chroot ( root) => {
333+ let original_path = append ( root. path ( ) , original_path) ;
334+ let link_path = append ( root. path ( ) , link) ;
335+ if is_dir {
336+ fs:: symlink_dir ( original_path, link_path) . await
337+ } else {
338+ fs:: symlink_file ( original_path, link_path) . await
339+ }
340+ } ,
341+ Inner :: Fake ( _) => panic ! ( "unimplemented" ) ,
342+ }
343+ }
344+
307345 /// Creates a new symbolic link on the filesystem.
308346 ///
309347 /// The `link` path will be a symbolic link pointing to the `original` path.
@@ -319,6 +357,52 @@ impl Fs {
319357 }
320358 }
321359
360+ /// Creates a new symbolic link on the filesystem.
361+ ///
362+ /// The `link` path will be a symbolic link pointing to the `original` path.
363+ ///
364+ /// This function works for both files and directories on all platforms.
365+ /// On Windows, it automatically detects whether the target is a file or directory
366+ /// and uses the appropriate system call.
367+ ///
368+ /// This is a proxy to [`std::os::windows::fs::symlink_file`] or [`std::os::windows::fs::symlink_dir`] on Windows,
369+ /// and [`std::os::unix::fs::symlink`] on Unix.
370+ #[ cfg( windows) ]
371+ pub fn symlink_sync ( & self , original : impl AsRef < Path > , link : impl AsRef < Path > ) -> io:: Result < ( ) > {
372+ use inner:: Inner ;
373+
374+ let original_path = original. as_ref ( ) ;
375+
376+ // Check if the original path exists and is a directory
377+ let is_dir = if let Ok ( metadata) = std:: fs:: metadata ( original_path) {
378+ metadata. is_dir ( )
379+ } else {
380+ // If the path doesn't exist, check if it ends with a path separator
381+ // This is a heuristic and not foolproof
382+ original_path. to_string_lossy ( ) . ends_with ( [ '/' , '\\' ] )
383+ } ;
384+
385+ match & self . 0 {
386+ Inner :: Real => {
387+ if is_dir {
388+ std:: os:: windows:: fs:: symlink_dir ( original_path, link)
389+ } else {
390+ std:: os:: windows:: fs:: symlink_file ( original_path, link)
391+ }
392+ } ,
393+ Inner :: Chroot ( root) => {
394+ let original_path = append ( root. path ( ) , original_path) ;
395+ let link_path = append ( root. path ( ) , link) ;
396+ if is_dir {
397+ std:: os:: windows:: fs:: symlink_dir ( original_path, link_path)
398+ } else {
399+ std:: os:: windows:: fs:: symlink_file ( original_path, link_path)
400+ }
401+ } ,
402+ Inner :: Fake ( _) => panic ! ( "unimplemented" ) ,
403+ }
404+ }
405+
322406 /// Query the metadata about a file without following symlinks.
323407 ///
324408 /// This is a proxy to [`tokio::fs::symlink_metadata`]
@@ -330,7 +414,6 @@ impl Fs {
330414 ///
331415 /// * The user lacks permissions to perform `metadata` call on `path`.
332416 /// * `path` does not exist.
333- #[ cfg( unix) ]
334417 pub async fn symlink_metadata ( & self , path : impl AsRef < Path > ) -> io:: Result < std:: fs:: Metadata > {
335418 use inner:: Inner ;
336419 match & self . 0 {
@@ -420,6 +503,7 @@ impl Shim for Fs {
420503/// Performs `a.join(b)`, except:
421504/// - if `b` is an absolute path, then the resulting path will equal `/a/b`
422505/// - if the prefix of `b` contains some `n` copies of a, then the resulting path will equal `/a/b`
506+ #[ cfg( unix) ]
423507fn append ( a : impl AsRef < Path > , b : impl AsRef < Path > ) -> PathBuf {
424508 use std:: ffi:: OsString ;
425509 use std:: os:: unix:: ffi:: OsStringExt ;
@@ -437,6 +521,80 @@ fn append(a: impl AsRef<Path>, b: impl AsRef<Path>) -> PathBuf {
437521 PathBuf :: from ( OsString :: from_vec ( a. to_vec ( ) ) ) . join ( PathBuf :: from ( OsString :: from_vec ( b. to_vec ( ) ) ) )
438522}
439523
524+ #[ cfg( windows) ]
525+ fn append ( a : impl AsRef < Path > , b : impl AsRef < Path > ) -> PathBuf {
526+ let a_path = a. as_ref ( ) ;
527+ let b_path = b. as_ref ( ) ;
528+
529+ // Convert paths to string representation with normalized separators
530+ let a_str = a_path. to_string_lossy ( ) . replace ( '/' , "\\ " ) ;
531+ let b_str = b_path. to_string_lossy ( ) . replace ( '/' , "\\ " ) ;
532+
533+ // Handle drive letters in Windows paths
534+ let ( b_drive, b_without_drive) = if b_str. len ( ) >= 2 && b_str. chars ( ) . nth ( 1 ) == Some ( ':' ) {
535+ let drive = & b_str[ ..2 ] ;
536+ let rest = & b_str[ 2 ..] ;
537+ ( Some ( drive) , rest. to_string ( ) )
538+ } else {
539+ ( None , b_str)
540+ } ;
541+
542+ // If b has a drive letter and it's different from a's drive letter (if any),
543+ // we need to handle it specially
544+ let result_path = if let Some ( b_drive) = b_drive {
545+ if a_str. starts_with ( b_drive) {
546+ // Same drive, continue with normal processing
547+ let path_str = b_without_drive;
548+
549+ // Repeatedly strip the prefix if b starts with a
550+ let a_without_drive = if a_str. len ( ) >= 2 && a_str. chars ( ) . nth ( 1 ) == Some ( ':' ) {
551+ & a_str[ 2 ..]
552+ } else {
553+ & a_str
554+ } ;
555+
556+ let mut b_normalized = path_str;
557+ while b_normalized. starts_with ( a_without_drive) {
558+ b_normalized = b_normalized[ a_without_drive. len ( ) ..] . to_string ( ) ;
559+ }
560+
561+ // Repeatedly strip leading backslashes
562+ while b_normalized. starts_with ( '\\' ) {
563+ b_normalized = b_normalized[ 1 ..] . to_string ( ) ;
564+ }
565+
566+ a_path. join ( b_normalized)
567+ } else {
568+ // Different drives, handle specially
569+ let mut path_str = b_without_drive;
570+
571+ // Repeatedly strip leading backslashes
572+ while path_str. starts_with ( '\\' ) {
573+ path_str = path_str[ 1 ..] . to_string ( ) ;
574+ }
575+
576+ a_path. join ( path_str)
577+ }
578+ } else {
579+ // No drive letter in b, proceed with normal processing
580+ let mut b_normalized = b_without_drive;
581+
582+ // Repeatedly strip the prefix if b starts with a
583+ while b_normalized. starts_with ( & a_str) {
584+ b_normalized = b_normalized[ a_str. len ( ) ..] . to_string ( ) ;
585+ }
586+
587+ // Repeatedly strip leading backslashes
588+ while b_normalized. starts_with ( '\\' ) {
589+ b_normalized = b_normalized[ 1 ..] . to_string ( ) ;
590+ }
591+
592+ a_path. join ( b_normalized)
593+ } ;
594+
595+ result_path
596+ }
597+
440598#[ cfg( test) ]
441599mod tests {
442600 use super :: * ;
@@ -478,10 +636,66 @@ mod tests {
478636 assert_eq!( append( $a, $b) , PathBuf :: from( $expected) ) ;
479637 } ;
480638 }
481- assert_append ! ( "/abc/test" , "/test" , "/abc/test/test" ) ;
482- assert_append ! ( "/tmp/.dir" , "/tmp/.dir/home/myuser" , "/tmp/.dir/home/myuser" ) ;
483- assert_append ! ( "/tmp/.dir" , "/tmp/hello" , "/tmp/.dir/tmp/hello" ) ;
484- assert_append ! ( "/tmp/.dir" , "/tmp/.dir/tmp/.dir/home/user" , "/tmp/.dir/home/user" ) ;
639+ #[ cfg( unix) ]
640+ {
641+ assert_append ! ( "/abc/test" , "/test" , "/abc/test/test" ) ;
642+ assert_append ! ( "/tmp/.dir" , "/tmp/.dir/home/myuser" , "/tmp/.dir/home/myuser" ) ;
643+ assert_append ! ( "/tmp/.dir" , "/tmp/hello" , "/tmp/.dir/tmp/hello" ) ;
644+ assert_append ! ( "/tmp/.dir" , "/tmp/.dir/tmp/.dir/home/user" , "/tmp/.dir/home/user" ) ;
645+ }
646+
647+ #[ cfg( windows) ]
648+ {
649+ // Basic path joining
650+ assert_append ! ( "C:\\ abc\\ test" , "test" , "C:\\ abc\\ test\\ test" ) ;
651+
652+ // Absolute path handling
653+ assert_append ! ( "C:\\ abc\\ test" , "C:\\ test" , "C:\\ abc\\ test\\ test" ) ;
654+
655+ // Nested path handling
656+ assert_append ! (
657+ "C:\\ tmp\\ .dir" ,
658+ "C:\\ tmp\\ .dir\\ home\\ myuser" ,
659+ "C:\\ tmp\\ .dir\\ home\\ myuser"
660+ ) ;
661+
662+ // Similar prefix handling
663+ assert_append ! ( "C:\\ tmp\\ .dir" , "C:\\ tmp\\ hello" , "C:\\ tmp\\ .dir\\ tmp\\ hello" ) ;
664+
665+ // Multiple prefixes handling
666+ assert_append ! (
667+ "C:\\ tmp\\ .dir" ,
668+ "C:\\ tmp\\ .dir\\ tmp\\ .dir\\ home\\ user" ,
669+ "C:\\ tmp\\ .dir\\ home\\ user"
670+ ) ;
671+
672+ // Different drive handling
673+ assert_append ! ( "C:\\ tmp" , "D:\\ data" , "C:\\ tmp\\ data" ) ;
674+
675+ // Forward slash handling in Windows paths
676+ assert_append ! ( "C:\\ tmp" , "C:/data/file.txt" , "C:\\ tmp\\ data\\ file.txt" ) ;
677+
678+ // UNC path handling
679+ assert_append ! (
680+ "C:\\ tmp" ,
681+ "\\ \\ server\\ share\\ file.txt" ,
682+ "C:\\ tmp\\ server\\ share\\ file.txt"
683+ ) ;
684+
685+ // Path with spaces
686+ assert_append ! (
687+ "C:\\ Program Files" ,
688+ "App Data\\ config.ini" ,
689+ "C:\\ Program Files\\ App Data\\ config.ini"
690+ ) ;
691+
692+ // Path with special characters
693+ assert_append ! (
694+ "C:\\ Users" ,
695+ 696+ "C:\\ Users\\ [email protected] \\ Documents" 697+ ) ;
698+ }
485699 }
486700
487701 #[ tokio:: test]
@@ -608,4 +822,58 @@ mod tests {
608822 fs. create_new ( "my_file.txt" ) . await . unwrap ( ) ;
609823 assert ! ( fs. create_new( "my_file.txt" ) . await . is_err( ) ) ;
610824 }
825+
826+ #[ tokio:: test]
827+ #[ cfg( windows) ]
828+ async fn test_unified_symlink_windows ( ) {
829+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
830+ let fs = Fs :: new ( ) ;
831+
832+ // Create a test file
833+ let file_path = dir. path ( ) . join ( "test_file.txt" ) ;
834+ fs. write ( & file_path, "test content" ) . await . unwrap ( ) ;
835+
836+ // Create a test directory
837+ let dir_path = dir. path ( ) . join ( "test_dir" ) ;
838+ fs. create_dir ( & dir_path) . await . unwrap ( ) ;
839+
840+ // Test symlink to file
841+ let file_link_path = dir. path ( ) . join ( "file_link" ) ;
842+ match fs. symlink ( & file_path, & file_link_path) . await {
843+ Ok ( _) => {
844+ // If we have permission to create symlinks, run the full test
845+ assert_eq ! ( fs. read_to_string( & file_link_path) . await . unwrap( ) , "test content" ) ;
846+
847+ // Test symlink to directory
848+ let dir_link_path = dir. path ( ) . join ( "dir_link" ) ;
849+ fs. symlink ( & dir_path, & dir_link_path) . await . unwrap ( ) ;
850+ assert ! ( fs. try_exists( & dir_link_path) . await . unwrap( ) ) ;
851+
852+ // Test symlink_sync to file
853+ let file_link_sync_path = dir. path ( ) . join ( "file_link_sync" ) ;
854+ fs. symlink_sync ( & file_path, & file_link_sync_path) . unwrap ( ) ;
855+ assert_eq ! ( fs. read_to_string( & file_link_sync_path) . await . unwrap( ) , "test content" ) ;
856+
857+ // Test symlink_sync to directory
858+ let dir_link_sync_path = dir. path ( ) . join ( "dir_link_sync" ) ;
859+ fs. symlink_sync ( & dir_path, & dir_link_sync_path) . unwrap ( ) ;
860+ assert ! ( fs. try_exists( & dir_link_sync_path) . await . unwrap( ) ) ;
861+
862+ // Clean up
863+ fs. remove_file ( & file_link_path) . await . unwrap ( ) ;
864+ fs. remove_file ( & file_link_sync_path) . await . unwrap ( ) ;
865+ fs. remove_dir_all ( & dir_link_path) . await . unwrap ( ) ;
866+ fs. remove_dir_all ( & dir_link_sync_path) . await . unwrap ( ) ;
867+ } ,
868+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: PermissionDenied || e. raw_os_error ( ) == Some ( 1314 ) => {
869+ // Error code 1314 is "A required privilege is not held by the client"
870+ // Skip the test if we don't have permission to create symlinks
871+ println ! ( "Skipping test_unified_symlink_windows: requires admin privileges on Windows" ) ;
872+ } ,
873+ Err ( e) => {
874+ // For other errors, fail the test
875+ panic ! ( "Unexpected error creating symlink: {}" , e) ;
876+ } ,
877+ }
878+ }
611879}
0 commit comments