@@ -3006,3 +3006,157 @@ fn test_mv_cross_device_file_symlink_preserved() {
30063006 "target content"
30073007 ) ;
30083008}
3009+
3010+ /// Find a supplementary group that differs from `current`.
3011+ /// Non-root users can chgrp to any group they belong to.
3012+ #[ cfg( target_os = "linux" ) ]
3013+ fn find_other_group ( current : u32 ) -> Option < u32 > {
3014+ nix:: unistd:: getgroups ( ) . ok ( ) ?. iter ( ) . find_map ( |group| {
3015+ let gid = group. as_raw ( ) ;
3016+ ( gid != current) . then_some ( gid)
3017+ } )
3018+ }
3019+
3020+ /// Test that group ownership is preserved during cross-device file moves.
3021+ /// Uses chgrp to a supplementary group (no root needed).
3022+ /// See https://github.com/uutils/coreutils/issues/9714
3023+ #[ test]
3024+ #[ cfg( target_os = "linux" ) ]
3025+ fn test_mv_cross_device_preserves_ownership ( ) {
3026+ use std:: fs;
3027+ use std:: os:: unix:: fs:: MetadataExt ;
3028+ use tempfile:: TempDir ;
3029+
3030+ let scene = TestScenario :: new ( util_name ! ( ) ) ;
3031+ let at = & scene. fixtures ;
3032+
3033+ at. write ( "owned_file" , "owned content" ) ;
3034+ let file_path = at. plus ( "owned_file" ) ;
3035+
3036+ let original_meta = fs:: metadata ( & file_path) . expect ( "Failed to get metadata" ) ;
3037+ let Some ( other_gid) = find_other_group ( original_meta. gid ( ) ) else {
3038+ println ! ( "SKIPPED: no supplementary group available for chgrp" ) ;
3039+ return ;
3040+ } ;
3041+
3042+ // chgrp to a different group (non-root users can do this for their own groups)
3043+ uucore:: perms:: wrap_chown (
3044+ & file_path,
3045+ & original_meta,
3046+ None ,
3047+ Some ( other_gid) ,
3048+ false ,
3049+ uucore:: perms:: Verbosity :: default ( ) ,
3050+ )
3051+ . expect ( "Failed to chgrp file" ) ;
3052+
3053+ let meta = fs:: metadata ( & file_path) . expect ( "Failed to get metadata" ) ;
3054+ assert_eq ! ( meta. gid( ) , other_gid, "chgrp should have changed the gid" ) ;
3055+
3056+ // Force cross-filesystem move using /dev/shm (tmpfs)
3057+ let target_dir =
3058+ TempDir :: new_in ( "/dev/shm/" ) . expect ( "Unable to create temp directory in /dev/shm" ) ;
3059+ let target_file = target_dir. path ( ) . join ( "owned_file" ) ;
3060+
3061+ scene
3062+ . ucmd ( )
3063+ . arg ( "owned_file" )
3064+ . arg ( target_file. to_str ( ) . unwrap ( ) )
3065+ . succeeds ( )
3066+ . no_stderr ( ) ;
3067+
3068+ let moved_meta = fs:: metadata ( & target_file) . expect ( "Failed to get metadata of moved file" ) ;
3069+ assert_eq ! (
3070+ moved_meta. gid( ) ,
3071+ other_gid,
3072+ "gid should be preserved after cross-device move (expected {other_gid}, got {})" ,
3073+ moved_meta. gid( )
3074+ ) ;
3075+ assert_eq ! (
3076+ moved_meta. uid( ) ,
3077+ meta. uid( ) ,
3078+ "uid should be preserved after cross-device move"
3079+ ) ;
3080+ }
3081+
3082+ /// Test that group ownership is preserved for files inside directories during cross-device moves.
3083+ /// Uses chgrp to a supplementary group (no root needed).
3084+ /// See https://github.com/uutils/coreutils/issues/9714
3085+ #[ test]
3086+ #[ cfg( target_os = "linux" ) ]
3087+ fn test_mv_cross_device_preserves_ownership_recursive ( ) {
3088+ use std:: fs;
3089+ use std:: os:: unix:: fs:: MetadataExt ;
3090+ use tempfile:: TempDir ;
3091+
3092+ let scene = TestScenario :: new ( util_name ! ( ) ) ;
3093+ let at = & scene. fixtures ;
3094+
3095+ at. mkdir ( "owned_dir" ) ;
3096+ at. mkdir ( "owned_dir/sub" ) ;
3097+ at. write ( "owned_dir/file1" , "content1" ) ;
3098+ at. write ( "owned_dir/sub/file2" , "content2" ) ;
3099+
3100+ let dir_meta = fs:: metadata ( at. plus ( "owned_dir" ) ) . expect ( "Failed to get metadata" ) ;
3101+ let Some ( other_gid) = find_other_group ( dir_meta. gid ( ) ) else {
3102+ println ! ( "SKIPPED: no supplementary group available for chgrp" ) ;
3103+ return ;
3104+ } ;
3105+
3106+ // chgrp all entries to a different group
3107+ for path in & [
3108+ "owned_dir" ,
3109+ "owned_dir/sub" ,
3110+ "owned_dir/file1" ,
3111+ "owned_dir/sub/file2" ,
3112+ ] {
3113+ let p = at. plus ( path) ;
3114+ let m = fs:: metadata ( & p) . expect ( "Failed to get metadata" ) ;
3115+ uucore:: perms:: wrap_chown (
3116+ & p,
3117+ & m,
3118+ None ,
3119+ Some ( other_gid) ,
3120+ false ,
3121+ uucore:: perms:: Verbosity :: default ( ) ,
3122+ )
3123+ . expect ( "Failed to chgrp" ) ;
3124+ }
3125+
3126+ // Force cross-filesystem move using /dev/shm (tmpfs)
3127+ let target_dir =
3128+ TempDir :: new_in ( "/dev/shm/" ) . expect ( "Unable to create temp directory in /dev/shm" ) ;
3129+ let target_path = target_dir. path ( ) . join ( "owned_dir" ) ;
3130+
3131+ scene
3132+ . ucmd ( )
3133+ . arg ( "owned_dir" )
3134+ . arg ( target_path. to_str ( ) . unwrap ( ) )
3135+ . succeeds ( )
3136+ . no_stderr ( ) ;
3137+
3138+ // Check ownership of the directory itself
3139+ let moved_dir_meta = fs:: metadata ( & target_path) . expect ( "Failed to get dir metadata" ) ;
3140+ assert_eq ! (
3141+ moved_dir_meta. gid( ) ,
3142+ other_gid,
3143+ "directory gid should be preserved after cross-device move"
3144+ ) ;
3145+
3146+ // Check ownership of a file inside the directory
3147+ let file1_meta = fs:: metadata ( target_path. join ( "file1" ) ) . expect ( "Failed to get file1 metadata" ) ;
3148+ assert_eq ! (
3149+ file1_meta. gid( ) ,
3150+ other_gid,
3151+ "file gid should be preserved after cross-device move"
3152+ ) ;
3153+
3154+ // Check ownership of a file in a subdirectory
3155+ let file2_meta =
3156+ fs:: metadata ( target_path. join ( "sub/file2" ) ) . expect ( "Failed to get file2 metadata" ) ;
3157+ assert_eq ! (
3158+ file2_meta. gid( ) ,
3159+ other_gid,
3160+ "nested file gid should be preserved after cross-device move"
3161+ ) ;
3162+ }
0 commit comments