@@ -967,7 +967,8 @@ fn rename_dir_fallback(
967967 ( _, _) => None ,
968968 } ;
969969
970- // Retrieve xattrs using file descriptor to avoid TOCTOU races
970+ // Retrieve xattrs before copying (directories use path-based operations
971+ // since they cannot be opened in write mode for xattr operations)
971972 #[ cfg( all( unix, not( any( target_os = "macos" , target_os = "redox" ) ) ) ) ]
972973 let xattrs = {
973974 use std:: fs:: File ;
@@ -989,12 +990,12 @@ fn rename_dir_fallback(
989990 display_manager,
990991 ) ;
991992
992- // Apply xattrs using file descriptor to avoid TOCTOU races
993+ // Apply xattrs after directory contents are copied, ignoring ENOTSUP errors
994+ // (filesystem doesn't support xattrs, which is acceptable for cross-device moves)
993995 #[ cfg( all( unix, not( any( target_os = "macos" , target_os = "redox" ) ) ) ) ]
994- {
995- use std:: fs:: OpenOptions ;
996- if let Ok ( f) = OpenOptions :: new ( ) . write ( true ) . open ( to) {
997- fsxattr:: apply_xattrs_fd ( & f, xattrs) ?;
996+ if let Err ( e) = fsxattr:: apply_xattrs ( to, xattrs) {
997+ if e. raw_os_error ( ) != Some ( libc:: EOPNOTSUPP ) {
998+ return Err ( e) ;
998999 }
9991000 }
10001001
@@ -1063,8 +1064,35 @@ fn copy_dir_contents_recursive(
10631064 pb. set_message ( from_path. to_string_lossy ( ) . to_string ( ) ) ;
10641065 }
10651066
1066- if from_path. is_dir ( ) {
1067- // Recursively copy subdirectory
1067+ if from_path. is_symlink ( ) {
1068+ // Handle symlinks first, before checking is_dir() which follows symlinks.
1069+ // This prevents symlinks to directories from being expanded into full copies.
1070+ #[ cfg( unix) ]
1071+ {
1072+ copy_file_with_hardlinks_helper (
1073+ & from_path,
1074+ & to_path,
1075+ hardlink_tracker,
1076+ hardlink_scanner,
1077+ ) ?;
1078+ }
1079+ #[ cfg( not( unix) ) ]
1080+ {
1081+ rename_symlink_fallback ( & from_path, & to_path) ?;
1082+ }
1083+
1084+ // Print verbose message for symlink
1085+ if verbose {
1086+ let message = translate ! ( "mv-verbose-renamed" , "from" => from_path. quote( ) , "to" => to_path. quote( ) ) ;
1087+ match display_manager {
1088+ Some ( pb) => pb. suspend ( || {
1089+ println ! ( "{message}" ) ;
1090+ } ) ,
1091+ None => println ! ( "{message}" ) ,
1092+ }
1093+ }
1094+ } else if from_path. is_dir ( ) {
1095+ // Recursively copy subdirectory (only real directories, not symlinks)
10681096 fs:: create_dir_all ( & to_path) ?;
10691097
10701098 // Print verbose message for directory
0 commit comments