+
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
20 changes: 19 additions & 1 deletion src/uu/cp/src/copydir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ impl<'a> Context<'a> {
let root_path = current_dir.join(root);
let root_parent = if target.exists() && !root.to_str().unwrap().ends_with("/.") {
root_path.parent().map(|p| p.to_path_buf())
} else if root == Path::new(".") && target.is_dir() {
// Special case: when copying current directory (.) to an existing directory,
// we don't want to use the parent path as root_parent because we want to
// copy the contents of the current directory directly into the target directory,
// not create a subdirectory with the current directory's name.
None
} else {
Some(root_path)
};
Expand Down Expand Up @@ -193,6 +199,17 @@ impl Entry {
} else {
descendant = descendant.strip_prefix(context.root)?.to_path_buf();
}
} else if context.root == Path::new(".") && context.target.is_dir() {
// Special case: when copying current directory (.) to an existing directory,
// strip the current directory name from the descendant path to avoid creating
// an extra level of nesting. For example, if we're in /home/user/source_dir
// and copying . to /home/user/dest_dir, we want to copy source_dir/file.txt
// to dest_dir/file.txt, not dest_dir/source_dir/file.txt.
if let Some(current_dir_name) = context.current_dir.file_name() {
if let Ok(stripped) = descendant.strip_prefix(current_dir_name) {
descendant = stripped.to_path_buf();
}
}
}

let local_to_target = context.target.join(descendant);
Expand Down Expand Up @@ -325,7 +342,8 @@ pub(crate) fn copy_directory(

// check if root is a prefix of target
if path_has_prefix(target, root)? {
return Err(translate!("cp-error-cannot-copy-directory-into-itself", "source" => root.quote(), "dest" => target.join(root.file_name().unwrap()).quote())
let dest_name = root.file_name().unwrap_or(root.as_os_str());
return Err(translate!("cp-error-cannot-copy-directory-into-itself", "source" => root.quote(), "dest" => target.join(dest_name).quote())
.into());
}

Expand Down
7 changes: 7 additions & 0 deletions src/uu/cp/src/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1440,6 +1440,13 @@ fn construct_dest_path(
Path::new("")
}
} else {
if source_path == Path::new(".") && target.is_dir() {
// Special case: when copying current directory (.) to an existing directory,
// return the target path directly instead of trying to construct a path
// relative to the source's parent. This ensures we copy the contents of
// the current directory into the target directory, not create a subdirectory.
return Ok(target.to_path_buf());
}
source_path.parent().unwrap_or(source_path)
};
localize_to_target(root, source_path, target)?
Expand Down
213 changes: 213 additions & 0 deletions tests/by-util/test_cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6847,6 +6847,219 @@ fn test_cp_preserve_context_root() {
}
}

// Test copying current directory (.) to an existing directory.
// This tests the special case where we copy the current directory
// to an existing directory, ensuring the directory name is properly
// stripped from the descendant path.
#[test]
fn test_cp_current_directory_to_existing_directory() {
let (at, mut ucmd) = at_and_ucmd!();

// Create source directory with files
at.mkdir("source_dir");
at.touch("source_dir/file1.txt");
at.touch("source_dir/file2.txt");
at.mkdir("source_dir/subdir");
at.touch("source_dir/subdir/file3.txt");

// Create existing destination directory
at.mkdir("dest_dir");

// Copy current directory (.) to existing directory
// This should copy the contents of source_dir to dest_dir
ucmd.current_dir(at.plus("source_dir"))
.args(&["-r", ".", "../dest_dir"])
.succeeds();

// Verify files were copied correctly
assert!(at.file_exists("dest_dir/file1.txt"));
assert!(at.file_exists("dest_dir/file2.txt"));
assert!(at.dir_exists("dest_dir/subdir"));
assert!(at.file_exists("dest_dir/subdir/file3.txt"));

// Verify the directory structure is correct (no extra nesting)
assert!(!at.file_exists("dest_dir/source_dir/file1.txt"));
}

// Test copying current directory (.) to a new directory.
// This should create the new directory and copy contents.
#[test]
fn test_cp_current_directory_to_new_directory() {
let (at, mut ucmd) = at_and_ucmd!();

// Create source directory with files
at.mkdir("source_dir");
at.touch("source_dir/file1.txt");
at.touch("source_dir/file2.txt");
at.mkdir("source_dir/subdir");
at.touch("source_dir/subdir/file3.txt");

// Copy current directory (.) to new directory
ucmd.current_dir(at.plus("source_dir"))
.args(&["-r", ".", "../new_dest_dir"])
.succeeds();

// Verify the new directory was created
assert!(at.dir_exists("new_dest_dir"));

// Verify files were copied correctly
assert!(at.file_exists("new_dest_dir/file1.txt"));
assert!(at.file_exists("new_dest_dir/file2.txt"));
assert!(at.dir_exists("new_dest_dir/subdir"));
assert!(at.file_exists("new_dest_dir/subdir/file3.txt"));
}

// Test copying current directory (.) with verbose output.
// This ensures the verbose output shows the correct paths.
#[test]
fn test_cp_current_directory_verbose() {
let (at, mut ucmd) = at_and_ucmd!();

// Create source directory with files
at.mkdir("source_dir");
at.touch("source_dir/file1.txt");
at.touch("source_dir/file2.txt");

// Create existing destination directory
at.mkdir("dest_dir");

// Copy current directory (.) to existing directory with verbose output
let result = ucmd
.current_dir(at.plus("source_dir"))
.args(&["-rv", ".", "../dest_dir"])
.succeeds();

// Verify files were copied
assert!(at.file_exists("dest_dir/file1.txt"));
assert!(at.file_exists("dest_dir/file2.txt"));

// Check that verbose output shows correct paths
let output = result.stdout_str();
// The verbose output should show the files being copied
// The exact path format may vary, so we check for the file names
assert!(output.contains("file1.txt"));
assert!(output.contains("file2.txt"));
// Also check that the destination directory is mentioned
assert!(output.contains("dest_dir"));
}

// Test copying current directory (.) with preserve attributes.
// This ensures attributes are preserved when copying the current directory.
#[test]
#[cfg(all(not(windows), not(target_os = "freebsd")))]
fn test_cp_current_directory_preserve_attributes() {
use filetime::FileTime;
use std::os::unix::prelude::MetadataExt;

let (at, mut ucmd) = at_and_ucmd!();

// Create source directory with files
at.mkdir("source_dir");
at.touch("source_dir/file1.txt");
at.touch("source_dir/file2.txt");

// Set specific permissions on the source files
at.set_mode("source_dir/file1.txt", 0o644);
at.set_mode("source_dir/file2.txt", 0o755);

// Set specific timestamps on the source files (1 hour ago)
let ts = time::OffsetDateTime::now_utc();
let previous = FileTime::from_unix_time(ts.unix_timestamp() - 3600, ts.nanosecond());
filetime::set_file_times(at.plus("source_dir/file1.txt"), previous, previous).unwrap();
filetime::set_file_times(at.plus("source_dir/file2.txt"), previous, previous).unwrap();

// Create existing destination directory
at.mkdir("dest_dir");

// Copy current directory (.) with preserve attributes
ucmd.current_dir(at.plus("source_dir"))
.args(&["-rp", ".", "../dest_dir"])
.succeeds();

// Verify files were copied
assert!(at.file_exists("dest_dir/file1.txt"));
assert!(at.file_exists("dest_dir/file2.txt"));

// Verify that permissions are preserved
let src_metadata1 = at.metadata("source_dir/file1.txt");
let dst_metadata1 = at.metadata("dest_dir/file1.txt");
assert_eq!(
src_metadata1.mode() & 0o7777,
dst_metadata1.mode() & 0o7777,
"file1.txt permissions not preserved"
);

let src_metadata2 = at.metadata("source_dir/file2.txt");
let dst_metadata2 = at.metadata("dest_dir/file2.txt");
assert_eq!(
src_metadata2.mode() & 0o7777,
dst_metadata2.mode() & 0o7777,
"file2.txt permissions not preserved"
);

// Verify that timestamps are preserved
let src_modified1 = src_metadata1.modified().unwrap();
let dst_modified1 = dst_metadata1.modified().unwrap();
assert_eq!(
src_modified1, dst_modified1,
"file1.txt timestamps not preserved"
);

let src_modified2 = src_metadata2.modified().unwrap();
let dst_modified2 = dst_metadata2.modified().unwrap();
assert_eq!(
src_modified2, dst_modified2,
"file2.txt timestamps not preserved"
);
}

// Test that copying current directory (.) to itself is disallowed.
// This should fail with an appropriate error message.
#[test]
fn test_cp_current_directory_to_itself_disallowed() {
let (at, mut ucmd) = at_and_ucmd!();

// Create a directory
at.mkdir("test_dir");
at.touch("test_dir/file1.txt");

// Try to copy current directory (.) to itself
ucmd.current_dir(at.plus("test_dir"))
.args(&["-r", ".", "."])
.fails()
.stderr_contains("cannot copy a directory");
}

// Test copying current directory (.) with symlinks.
// This ensures symlinks are handled correctly when copying the current directory.
#[test]
fn test_cp_current_directory_with_symlinks() {
let (at, mut ucmd) = at_and_ucmd!();

// Create source directory with files and symlinks
at.mkdir("source_dir");
at.touch("source_dir/file1.txt");
at.symlink_file("file1.txt", "source_dir/link1.txt");
at.mkdir("source_dir/subdir");
at.touch("source_dir/subdir/file2.txt");
at.symlink_file("../file1.txt", "source_dir/subdir/link2.txt");

// Create existing destination directory
at.mkdir("dest_dir");

// Copy current directory (.) to existing directory
ucmd.current_dir(at.plus("source_dir"))
.args(&["-r", ".", "../dest_dir"])
.succeeds();

// Verify files and symlinks were copied correctly
assert!(at.file_exists("dest_dir/file1.txt"));
assert!(at.is_symlink("dest_dir/link1.txt"));
assert!(at.dir_exists("dest_dir/subdir"));
assert!(at.file_exists("dest_dir/subdir/file2.txt"));
assert!(at.is_symlink("dest_dir/subdir/link2.txt"));
}

#[test]
#[cfg(not(windows))]
fn test_cp_no_dereference_symlink_with_parents() {
Expand Down
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载