1
1
use std:: collections:: HashMap ;
2
2
use std:: fs:: Permissions ;
3
3
use std:: io;
4
+ #[ cfg( unix) ]
4
5
use 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 } ;
13
8
14
9
use tempfile:: TempDir ;
15
10
use tokio:: fs;
@@ -22,10 +17,7 @@ pub struct Fs(inner::Inner);
22
17
mod inner {
23
18
use std:: collections:: HashMap ;
24
19
use std:: path:: PathBuf ;
25
- use std:: sync:: {
26
- Arc ,
27
- Mutex ,
28
- } ;
20
+ use std:: sync:: { Arc , Mutex } ;
29
21
30
22
use tempfile:: TempDir ;
31
23
@@ -304,6 +296,52 @@ impl Fs {
304
296
}
305
297
}
306
298
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
+
307
345
/// Creates a new symbolic link on the filesystem.
308
346
///
309
347
/// The `link` path will be a symbolic link pointing to the `original` path.
@@ -319,6 +357,52 @@ impl Fs {
319
357
}
320
358
}
321
359
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
+
322
406
/// Query the metadata about a file without following symlinks.
323
407
///
324
408
/// This is a proxy to [`tokio::fs::symlink_metadata`]
@@ -330,7 +414,6 @@ impl Fs {
330
414
///
331
415
/// * The user lacks permissions to perform `metadata` call on `path`.
332
416
/// * `path` does not exist.
333
- #[ cfg( unix) ]
334
417
pub async fn symlink_metadata ( & self , path : impl AsRef < Path > ) -> io:: Result < std:: fs:: Metadata > {
335
418
use inner:: Inner ;
336
419
match & self . 0 {
@@ -420,6 +503,7 @@ impl Shim for Fs {
420
503
/// Performs `a.join(b)`, except:
421
504
/// - if `b` is an absolute path, then the resulting path will equal `/a/b`
422
505
/// - if the prefix of `b` contains some `n` copies of a, then the resulting path will equal `/a/b`
506
+ #[ cfg( unix) ]
423
507
fn append ( a : impl AsRef < Path > , b : impl AsRef < Path > ) -> PathBuf {
424
508
use std:: ffi:: OsString ;
425
509
use std:: os:: unix:: ffi:: OsStringExt ;
@@ -437,6 +521,80 @@ fn append(a: impl AsRef<Path>, b: impl AsRef<Path>) -> PathBuf {
437
521
PathBuf :: from ( OsString :: from_vec ( a. to_vec ( ) ) ) . join ( PathBuf :: from ( OsString :: from_vec ( b. to_vec ( ) ) ) )
438
522
}
439
523
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
+
440
598
#[ cfg( test) ]
441
599
mod tests {
442
600
use super :: * ;
@@ -478,10 +636,66 @@ mod tests {
478
636
assert_eq!( append( $a, $b) , PathBuf :: from( $expected) ) ;
479
637
} ;
480
638
}
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
+ "user.name@domain.com\\ Documents" ,
696
+ "C:\\ Users\\ user.name@domain.com\\ Documents"
697
+ ) ;
698
+ }
485
699
}
486
700
487
701
#[ tokio:: test]
@@ -608,4 +822,58 @@ mod tests {
608
822
fs. create_new ( "my_file.txt" ) . await . unwrap ( ) ;
609
823
assert ! ( fs. create_new( "my_file.txt" ) . await . is_err( ) ) ;
610
824
}
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
+ }
611
879
}
0 commit comments