@@ -907,6 +907,8 @@ fn rename_symlink_fallback(from: &Path, to: &Path) -> io::Result<()> {
907907 {
908908 let _ = copy_xattrs_if_supported ( from, to) ;
909909 }
910+ // Preserve ownership (uid/gid) from the source symlink
911+ let _ = preserve_ownership ( from, to) ;
910912 fs:: remove_file ( from)
911913}
912914
@@ -1005,6 +1007,12 @@ fn copy_dir_contents(
10051007 // Create the destination directory
10061008 fs:: create_dir_all ( to) ?;
10071009
1010+ // Preserve ownership (uid/gid) of the top-level directory
1011+ #[ cfg( unix) ]
1012+ {
1013+ let _ = preserve_ownership ( from, to) ;
1014+ }
1015+
10081016 // Recursively copy contents
10091017 #[ cfg( unix) ]
10101018 {
@@ -1085,6 +1093,12 @@ fn copy_dir_contents_recursive(
10851093 // Recursively copy subdirectory (only real directories, not symlinks)
10861094 fs:: create_dir_all ( & to_path) ?;
10871095
1096+ // Preserve ownership (uid/gid) of the subdirectory
1097+ #[ cfg( unix) ]
1098+ {
1099+ let _ = preserve_ownership ( & from_path, & to_path) ;
1100+ }
1101+
10881102 print_verbose ( & from_path, & to_path) ;
10891103
10901104 copy_dir_contents_recursive (
@@ -1148,9 +1162,12 @@ fn copy_file_with_hardlinks_helper(
11481162
11491163 if from. is_symlink ( ) {
11501164 // Copy a symlink file (no-follow).
1165+ // rename_symlink_fallback already preserves ownership and removes the source.
11511166 rename_symlink_fallback ( from, to) ?;
11521167 } else if is_fifo ( from. symlink_metadata ( ) ?. file_type ( ) ) {
11531168 make_fifo ( to) ?;
1169+ // Preserve ownership (uid/gid) from the source
1170+ let _ = preserve_ownership ( from, to) ;
11541171 } else {
11551172 // Copy a regular file.
11561173 fs:: copy ( from, to) ?;
@@ -1159,6 +1176,8 @@ fn copy_file_with_hardlinks_helper(
11591176 {
11601177 let _ = copy_xattrs_if_supported ( from, to) ;
11611178 }
1179+ // Preserve ownership (uid/gid) from the source
1180+ let _ = preserve_ownership ( from, to) ;
11621181 }
11631182
11641183 Ok ( ( ) )
@@ -1208,11 +1227,54 @@ fn rename_file_fallback(
12081227 let _ = copy_xattrs_if_supported ( from, to) ;
12091228 }
12101229
1230+ // Preserve ownership (uid/gid) from the source file
1231+ #[ cfg( unix) ]
1232+ {
1233+ let _ = preserve_ownership ( from, to) ;
1234+ }
1235+
12111236 fs:: remove_file ( from)
12121237 . map_err ( |err| io:: Error :: new ( err. kind ( ) , translate ! ( "mv-error-permission-denied" ) ) ) ?;
12131238 Ok ( ( ) )
12141239}
12151240
1241+ /// Preserve ownership (uid/gid) from source to destination.
1242+ /// Uses lchown so it works on symlinks without following them.
1243+ /// Errors are silently ignored for non-root users who cannot chown.
1244+ #[ cfg( unix) ]
1245+ fn preserve_ownership ( from : & Path , to : & Path ) -> io:: Result < ( ) > {
1246+ use std:: os:: unix:: fs:: MetadataExt ;
1247+
1248+ let source_meta = from. symlink_metadata ( ) ?;
1249+ let uid = source_meta. uid ( ) ;
1250+ let gid = source_meta. gid ( ) ;
1251+
1252+ let dest_meta = to. symlink_metadata ( ) ?;
1253+ let dest_uid = dest_meta. uid ( ) ;
1254+ let dest_gid = dest_meta. gid ( ) ;
1255+
1256+ // Only chown if ownership actually differs
1257+ if uid != dest_uid || gid != dest_gid {
1258+ use uucore:: perms:: { Verbosity , VerbosityLevel , wrap_chown} ;
1259+ // Use follow=false so lchown is used (works on symlinks)
1260+ // Silently ignore errors: non-root users typically cannot chown to
1261+ // arbitrary uid, matching GNU mv behavior which also uses best-effort.
1262+ let _ = wrap_chown (
1263+ to,
1264+ & dest_meta,
1265+ Some ( uid) ,
1266+ Some ( gid) ,
1267+ false ,
1268+ Verbosity {
1269+ groups_only : false ,
1270+ level : VerbosityLevel :: Silent ,
1271+ } ,
1272+ ) ;
1273+ }
1274+
1275+ Ok ( ( ) )
1276+ }
1277+
12161278/// Copy xattrs from source to destination, ignoring ENOTSUP/EOPNOTSUPP errors.
12171279/// These errors indicate the filesystem doesn't support extended attributes,
12181280/// which is acceptable when moving files across filesystems.
0 commit comments