diff --git a/src/uu/cp/src/copydir.rs b/src/uu/cp/src/copydir.rs index 2543f37b143..3cdbd1d60f0 100644 --- a/src/uu/cp/src/copydir.rs +++ b/src/uu/cp/src/copydir.rs @@ -235,7 +235,18 @@ impl Entry { } } - let local_to_target = context.target.join(descendant); + // For the entry that maps onto the target itself, `join("")` appends a + // separator, which makes the symlink check in `copy_direntry` look through + // a target that is a symlink. Keep the target as given, except for `src/.` + // without `-T`: there the separator is wanted, since that copies into + // whatever the target resolves to. + let local_to_target = if descendant.as_os_str().is_empty() + && (no_target_dir || !ends_with_curdir(context.root)) + { + context.target.to_path_buf() + } else { + context.target.join(descendant) + }; let target_is_file = context.target_is_file; Ok(Self { source_absolute, diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 50788c0e9a0..1b5d926afc2 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -7785,6 +7785,63 @@ fn test_cp_recursive_dest_subdir_symlink_not_followed() { ); } +/// With `-T` the target is the destination itself, so a target that is a +/// symlink to a directory is a non-directory and must not be written through. +#[test] +#[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: symlink/hardlink capability restrictions cause dangling-symlink/same-file detection to differ" +)] +fn test_cp_recursive_no_target_dir_refuses_symlink_target() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir_all("src/sub"); + at.write("src/f", "SOURCE"); + at.mkdir("real"); + at.write("real/f", "REAL"); + at.symlink_dir("real", "dst"); + + for args in [["-rT", "src", "dst"], ["-rT", "src/.", "dst"]] { + scene + .ucmd() + .args(&args) + .fails() + .stderr_contains("cannot overwrite non-directory"); + } + assert_eq!(at.read("real/f"), "REAL"); + assert!(!at.dir_exists("real/sub")); + + // A trailing slash asks for what the symlink resolves to, as in GNU. + scene.ucmd().args(&["-rT", "src", "dst/"]).succeeds(); + assert_eq!(at.read("real/f"), "SOURCE"); +} + +/// A dangling symlink at the target is a non-directory too: the copy must not +/// create the directory it points at. +#[test] +#[cfg(unix)] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: symlink/hardlink capability restrictions cause dangling-symlink/same-file detection to differ" +)] +fn test_cp_recursive_refuses_dangling_symlink_target() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.mkdir("src"); + at.write("src/f", "SOURCE"); + at.symlink_dir("nowhere", "dst"); + + for flags in ["-r", "-rT"] { + scene + .ucmd() + .args(&[flags, "src", "dst"]) + .fails() + .stderr_contains("cannot overwrite non-directory"); + } + assert!(!at.dir_exists("nowhere")); +} + /// A symlinked directory named as the *target* is still a legitimate /// destination -- only entries discovered inside the tree are refused. #[test] @@ -9064,11 +9121,11 @@ fn test_cp_recurse_verbose_output() { let file = "file"; #[cfg(not(windows))] let output = format!( - "'{source_dir}' -> '{target_dir}/'\n'{source_dir}/{file}' -> '{target_dir}/{file}'\n" + "'{source_dir}' -> '{target_dir}'\n'{source_dir}/{file}' -> '{target_dir}/{file}'\n" ); #[cfg(windows)] let output = format!( - "'{source_dir}' -> '{target_dir}\\'\n'{source_dir}\\{file}' -> '{target_dir}\\{file}'\n" + "'{source_dir}' -> '{target_dir}'\n'{source_dir}\\{file}' -> '{target_dir}\\{file}'\n" ); let (at, mut ucmd) = at_and_ucmd!(); @@ -9096,11 +9153,11 @@ fn test_cp_recurse_verbose_output_with_symlink() { let symlink = "symlink"; #[cfg(not(windows))] let output = format!( - "'{source_dir}' -> '{target_dir}/'\n'{source_dir}/{symlink}' -> '{target_dir}/{symlink}'\n" + "'{source_dir}' -> '{target_dir}'\n'{source_dir}/{symlink}' -> '{target_dir}/{symlink}'\n" ); #[cfg(windows)] let output = format!( - "'{source_dir}' -> '{target_dir}\\'\n'{source_dir}\\{symlink}' -> '{target_dir}\\{symlink}'\n" + "'{source_dir}' -> '{target_dir}'\n'{source_dir}\\{symlink}' -> '{target_dir}\\{symlink}'\n" ); let (at, mut ucmd) = at_and_ucmd!();