Skip to content

Commit 3211d85

Browse files
committed
mv: add tests for cross-device move ownership preservation (#9714)
1 parent 66d4fae commit 3211d85

1 file changed

Lines changed: 154 additions & 0 deletions

File tree

tests/by-util/test_mv.rs

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)