@@ -15,8 +15,7 @@ use clap::{Arg, ArgAction, ArgMatches, Command};
1515use indicatif:: { MultiProgress , ProgressBar , ProgressStyle } ;
1616
1717#[ cfg( all( unix, not( any( target_os = "macos" , target_os = "redox" ) ) ) ) ]
18- use rustc_hash:: FxHashMap ;
19- use rustc_hash:: FxHashSet ;
18+ use rustc_hash:: { FxHashMap , FxHashSet } ;
2019use std:: env;
2120use std:: ffi:: OsString ;
2221use std:: fs;
@@ -967,7 +966,8 @@ fn rename_dir_fallback(
967966 ( _, _) => None ,
968967 } ;
969968
970- // Retrieve xattrs using file descriptor to avoid TOCTOU races
969+ // Retrieve xattrs before copying (directories use path-based operations
970+ // since they cannot be opened in write mode for xattr operations)
971971 #[ cfg( all( unix, not( any( target_os = "macos" , target_os = "redox" ) ) ) ) ]
972972 let xattrs = {
973973 use std:: fs:: File ;
@@ -989,12 +989,12 @@ fn rename_dir_fallback(
989989 display_manager,
990990 ) ;
991991
992- // Apply xattrs using file descriptor to avoid TOCTOU races
992+ // Apply xattrs after directory contents are copied, ignoring ENOTSUP errors
993+ // (filesystem doesn't support xattrs, which is acceptable for cross-device moves)
993994 #[ 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) ?;
995+ if let Err ( e) = fsxattr:: apply_xattrs ( to, xattrs) {
996+ if e. raw_os_error ( ) != Some ( libc:: EOPNOTSUPP ) {
997+ return Err ( e) ;
998998 }
999999 }
10001000
@@ -1063,8 +1063,35 @@ fn copy_dir_contents_recursive(
10631063 pb. set_message ( from_path. to_string_lossy ( ) . to_string ( ) ) ;
10641064 }
10651065
1066- if from_path. is_dir ( ) {
1067- // Recursively copy subdirectory
1066+ if from_path. is_symlink ( ) {
1067+ // Handle symlinks first, before checking is_dir() which follows symlinks.
1068+ // This prevents symlinks to directories from being expanded into full copies.
1069+ #[ cfg( unix) ]
1070+ {
1071+ copy_file_with_hardlinks_helper (
1072+ & from_path,
1073+ & to_path,
1074+ hardlink_tracker,
1075+ hardlink_scanner,
1076+ ) ?;
1077+ }
1078+ #[ cfg( not( unix) ) ]
1079+ {
1080+ rename_symlink_fallback ( & from_path, & to_path) ?;
1081+ }
1082+
1083+ // Print verbose message for symlink
1084+ if verbose {
1085+ let message = translate ! ( "mv-verbose-renamed" , "from" => from_path. quote( ) , "to" => to_path. quote( ) ) ;
1086+ match display_manager {
1087+ Some ( pb) => pb. suspend ( || {
1088+ println ! ( "{message}" ) ;
1089+ } ) ,
1090+ None => println ! ( "{message}" ) ,
1091+ }
1092+ }
1093+ } else if from_path. is_dir ( ) {
1094+ // Recursively copy subdirectory (only real directories, not symlinks)
10681095 fs:: create_dir_all ( & to_path) ?;
10691096
10701097 // Print verbose message for directory
0 commit comments