@@ -975,14 +975,10 @@ fn rename_dir_fallback(
975975 ( _, _) => None ,
976976 } ;
977977
978- // Retrieve xattrs using file descriptor to avoid TOCTOU races
978+ // Retrieve xattrs before copying (directories use path-based operations
979+ // since they cannot be opened in write mode for xattr operations)
979980 #[ cfg( all( unix, not( any( target_os = "macos" , target_os = "redox" ) ) ) ) ]
980- let xattrs = {
981- use std:: fs:: File ;
982- File :: open ( from)
983- . and_then ( |f| fsxattr:: retrieve_xattrs_fd ( & f) )
984- . unwrap_or_else ( |_| HashMap :: new ( ) )
985- } ;
981+ let xattrs = fsxattr:: retrieve_xattrs ( from) . unwrap_or_else ( |_| HashMap :: new ( ) ) ;
986982
987983 // Use directory copying (with or without hardlink support)
988984 let result = copy_dir_contents (
@@ -997,12 +993,12 @@ fn rename_dir_fallback(
997993 display_manager,
998994 ) ;
999995
1000- // Apply xattrs using file descriptor to avoid TOCTOU races
996+ // Apply xattrs after directory contents are copied, ignoring ENOTSUP errors
997+ // (filesystem doesn't support xattrs, which is acceptable for cross-device moves)
1001998 #[ cfg( all( unix, not( any( target_os = "macos" , target_os = "redox" ) ) ) ) ]
1002- {
1003- use std:: fs:: OpenOptions ;
1004- if let Ok ( f) = OpenOptions :: new ( ) . write ( true ) . open ( to) {
1005- fsxattr:: apply_xattrs_fd ( & f, xattrs) ?;
999+ if let Err ( e) = fsxattr:: apply_xattrs ( to, xattrs) {
1000+ if e. raw_os_error ( ) != Some ( libc:: EOPNOTSUPP ) {
1001+ return Err ( e) ;
10061002 }
10071003 }
10081004
@@ -1071,8 +1067,35 @@ fn copy_dir_contents_recursive(
10711067 pb. set_message ( from_path. to_string_lossy ( ) . to_string ( ) ) ;
10721068 }
10731069
1074- if from_path. is_dir ( ) {
1075- // Recursively copy subdirectory
1070+ if from_path. is_symlink ( ) {
1071+ // Handle symlinks first, before checking is_dir() which follows symlinks.
1072+ // This prevents symlinks to directories from being expanded into full copies.
1073+ #[ cfg( unix) ]
1074+ {
1075+ copy_file_with_hardlinks_helper (
1076+ & from_path,
1077+ & to_path,
1078+ hardlink_tracker,
1079+ hardlink_scanner,
1080+ ) ?;
1081+ }
1082+ #[ cfg( not( unix) ) ]
1083+ {
1084+ rename_symlink_fallback ( & from_path, & to_path) ?;
1085+ }
1086+
1087+ // Print verbose message for symlink
1088+ if verbose {
1089+ let message = translate ! ( "mv-verbose-renamed" , "from" => from_path. quote( ) , "to" => to_path. quote( ) ) ;
1090+ match display_manager {
1091+ Some ( pb) => pb. suspend ( || {
1092+ println ! ( "{message}" ) ;
1093+ } ) ,
1094+ None => println ! ( "{message}" ) ,
1095+ }
1096+ }
1097+ } else if from_path. is_dir ( ) {
1098+ // Recursively copy subdirectory (only real directories, not symlinks)
10761099 fs:: create_dir_all ( & to_path) ?;
10771100
10781101 // Print verbose message for directory
0 commit comments