Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/uu/cp/src/copydir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,9 @@ pub(crate) fn copy_directory(
let dest = target.join(root.file_name().unwrap());
copy_attributes(root, dest.as_path(), &options.attributes)?;
for (x, y) in aligned_ancestors(root, dest.as_path()) {
copy_attributes(x, y, &options.attributes)?;
if let Ok(src) = canonicalize(x, MissingHandling::Normal, ResolveMode::Physical) {
copy_attributes(&src, y, &options.attributes)?;
}
}
} else {
copy_attributes(root, target, &options.attributes)?;
Expand Down
28 changes: 19 additions & 9 deletions src/uu/cp/src/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1332,7 +1332,11 @@ fn construct_dest_path(
Ok(match target_type {
TargetType::Directory => {
let root = if options.parents {
Path::new("")
if source_path.has_root() && cfg!(unix) {
Path::new("/")
} else {
Path::new("")
}
} else {
source_path.parent().unwrap_or(source_path)
};
Expand Down Expand Up @@ -1380,7 +1384,9 @@ fn copy_source(
);
if options.parents {
for (x, y) in aligned_ancestors(source, dest.as_path()) {
copy_attributes(x, y, &options.attributes)?;
if let Ok(src) = canonicalize(x, MissingHandling::Normal, ResolveMode::Physical) {
copy_attributes(&src, y, &options.attributes)?;
}
}
}
res
Expand Down Expand Up @@ -1443,20 +1449,18 @@ pub(crate) fn copy_attributes(

let dest_uid = source_metadata.uid();
let dest_gid = source_metadata.gid();

wrap_chown(
// gnu compatibility: cp doesn't report an error if it fails to set the ownership.
let _ = wrap_chown(
dest,
&dest.symlink_metadata().context(context)?,
Some(dest_uid),
Some(dest_gid),
false,
Verbosity {
groups_only: false,
level: VerbosityLevel::Normal,
level: VerbosityLevel::Silent,
},
)
.map_err(Error::Error)?;

);
Ok(())
})?;

Expand Down Expand Up @@ -2163,7 +2167,13 @@ fn copy_file(
fs::set_permissions(dest, dest_permissions).ok();
}

copy_attributes(source, dest, &options.attributes)?;
if options.dereference(source_in_command_line) {
if let Ok(src) = canonicalize(source, MissingHandling::Normal, ResolveMode::Physical) {
copy_attributes(&src, dest, &options.attributes)?;
}
} else {
copy_attributes(source, dest, &options.attributes)?;
}

copied_files.insert(
FileInformation::from_path(source, options.dereference(source_in_command_line))?,
Expand Down
86 changes: 86 additions & 0 deletions tests/by-util/test_cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5573,3 +5573,89 @@ fn test_preserve_attrs_overriding_2() {
assert_ne!(dest_file1_metadata.ino(), dest_file2_metadata.ino());
}
}

/// Test the behavior of preserving permissions when copying through a symlink
#[test]
#[cfg(unix)]
fn test_cp_symlink_permissions() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch("a");
at.set_mode("a", 0o700);
at.symlink_file("a", "symlink");
at.mkdir("dest");
scene
.ucmd()
.args(&["--preserve", "symlink", "dest"])
.succeeds();
let dest_dir_metadata = at.metadata("dest/symlink");
let src_dir_metadata = at.metadata("a");
assert_eq!(
src_dir_metadata.permissions().mode(),
dest_dir_metadata.permissions().mode()
);
}

/// Test the behavior of preserving permissions of parents when copying through a symlink
#[test]
#[cfg(unix)]
fn test_cp_parents_symlink_permissions_file() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.mkdir("a");
at.touch("a/file");
at.set_mode("a", 0o700);
at.symlink_dir("a", "symlink");
at.mkdir("dest");
scene
.ucmd()
.args(&["--parents", "-a", "symlink/file", "dest"])
.succeeds();
let dest_dir_metadata = at.metadata("dest/symlink");
let src_dir_metadata = at.metadata("a");
assert_eq!(
src_dir_metadata.permissions().mode(),
dest_dir_metadata.permissions().mode()
);
}

/// Test the behavior of preserving permissions of parents when copying through
/// a symlink when source is a dir.
#[test]
#[cfg(unix)]
fn test_cp_parents_symlink_permissions_dir() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.mkdir_all("a/b");
at.set_mode("a", 0o755); // Set mode for the actual directory
at.symlink_dir("a", "symlink");
at.mkdir("dest");
scene
.ucmd()
.args(&["--parents", "-a", "symlink/b", "dest"])
.succeeds();
let dest_dir_metadata = at.metadata("dest/symlink");
let src_dir_metadata = at.metadata("a");
assert_eq!(
src_dir_metadata.permissions().mode(),
dest_dir_metadata.permissions().mode()
);
}

/// Test the behavior of copying a file to a destination with parents using absolute paths.
#[cfg(unix)]
#[test]
fn test_cp_parents_absolute_path() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.mkdir_all("a/b");
at.touch("a/b/f");
at.mkdir("dest");
let src = format!("{}/a/b/f", at.root_dir_resolved());
scene
.ucmd()
.args(&["--parents", src.as_str(), "dest"])
.succeeds();
let res = format!("dest{}/a/b/f", at.root_dir_resolved());
at.file_exists(res);
}