Skip to content

Commit 4543e4a

Browse files
committed
fix(cp): handle deleted working directory with absolute paths
Fixes issue #9105: cp now works with absolute source/target paths even when the current working directory has been deleted. Changes: - Make current_dir optional, only call env::current_dir() for relative paths - Replace unsafe unwrap() with safe is_some_and() path handling - Extract root_parent logic into determine_root_parent() helper method - Improve code clarity and maintainability This avoids getcwd() syscall failures and matches GNU cp behavior while maintaining full backward compatibility.
1 parent d432131 commit 4543e4a

File tree

2 files changed

+95
-17
lines changed

2 files changed

+95
-17
lines changed

src/uu/cp/src/copydir.rs

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ fn skip_last<T>(mut iter: impl Iterator<Item = T>) -> impl Iterator<Item = T> {
8989

9090
/// Paths that are invariant throughout the traversal when copying a directory.
9191
struct Context<'a> {
92-
/// The current working directory at the time of starting the traversal.
93-
current_dir: PathBuf,
92+
/// The current working directory at the time of starting the traversal (if root is relative).
93+
current_dir: Option<PathBuf>,
9494

9595
/// The path to the parent of the source directory, if any.
9696
root_parent: Option<PathBuf>,
@@ -107,20 +107,21 @@ struct Context<'a> {
107107

108108
impl<'a> Context<'a> {
109109
fn new(root: &'a Path, target: &'a Path) -> io::Result<Self> {
110-
let current_dir = env::current_dir()?;
111-
let root_path = current_dir.join(root);
112110
let target_is_file = target.is_file();
113-
let root_parent = if target.exists() && !root.to_str().unwrap().ends_with("/.") {
114-
root_path.parent().map(|p| p.to_path_buf())
115-
} else if root == Path::new(".") && target.is_dir() {
116-
// Special case: when copying current directory (.) to an existing directory,
117-
// we don't want to use the parent path as root_parent because we want to
118-
// copy the contents of the current directory directly into the target directory,
119-
// not create a subdirectory with the current directory's name.
120-
None
111+
112+
// Only get the current directory if root is a relative path.
113+
// This avoids unnecessary getcwd() syscall failures when the current
114+
// directory has been deleted (issue #9105), and matches GNU cp behavior.
115+
let (current_dir, root_path) = if root.is_absolute() {
116+
(None, root.to_path_buf())
121117
} else {
122-
Some(root_path)
118+
let cwd = env::current_dir()?;
119+
let root_path = cwd.join(root);
120+
(Some(cwd), root_path)
123121
};
122+
123+
let root_parent = Self::determine_root_parent(root, target, &root_path);
124+
124125
Ok(Self {
125126
current_dir,
126127
root_parent,
@@ -129,6 +130,26 @@ impl<'a> Context<'a> {
129130
root,
130131
})
131132
}
133+
134+
/// Determine root_parent for path resolution during traversal.
135+
fn determine_root_parent(root: &Path, target: &Path, root_path: &Path) -> Option<PathBuf> {
136+
// Check if root path ends with "/." (e.g., "dir/.")
137+
let root_ends_with_dot = root.to_str().is_some_and(|s| s.ends_with("/."));
138+
139+
if target.exists() && !root_ends_with_dot {
140+
// Normal case: use parent of resolved root path
141+
root_path.parent().map(|p| p.to_path_buf())
142+
} else if root == Path::new(".") && target.is_dir() {
143+
// Special case: when copying current directory (.) to an existing directory,
144+
// we don't want to use the parent path as root_parent because we want to
145+
// copy the contents of the current directory directly into the target directory,
146+
// not create a subdirectory with the current directory's name.
147+
None
148+
} else {
149+
// Use the root path itself when target doesn't exist yet
150+
Some(root_path.to_path_buf())
151+
}
152+
}
132153
}
133154

134155
/// Data needed to perform a single copy operation while traversing a directory.
@@ -188,7 +209,11 @@ impl Entry {
188209
) -> Result<Self, StripPrefixError> {
189210
let source = source.as_ref();
190211
let source_relative = source.to_path_buf();
191-
let source_absolute = context.current_dir.join(&source_relative);
212+
let source_absolute = if let Some(ref current_dir) = context.current_dir {
213+
current_dir.join(&source_relative)
214+
} else {
215+
source_relative.clone()
216+
};
192217
let mut descendant =
193218
get_local_to_root_parent(&source_absolute, context.root_parent.as_deref())?;
194219
if no_target_dir {
@@ -217,9 +242,11 @@ impl Entry {
217242
// an extra level of nesting. For example, if we're in /home/user/source_dir
218243
// and copying . to /home/user/dest_dir, we want to copy source_dir/file.txt
219244
// to dest_dir/file.txt, not dest_dir/source_dir/file.txt.
220-
if let Some(current_dir_name) = context.current_dir.file_name() {
221-
if let Ok(stripped) = descendant.strip_prefix(current_dir_name) {
222-
descendant = stripped.to_path_buf();
245+
if let Some(current_dir) = &context.current_dir {
246+
if let Some(current_dir_name) = current_dir.file_name() {
247+
if let Ok(stripped) = descendant.strip_prefix(current_dir_name) {
248+
descendant = stripped.to_path_buf();
249+
}
223250
}
224251
}
225252
}

tests/by-util/test_cp.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7227,3 +7227,54 @@ fn test_cp_recurse_verbose_output_with_symlink_already_exists() {
72277227
.no_stderr()
72287228
.stdout_is(output);
72297229
}
7230+
7231+
#[test]
7232+
#[cfg(unix)]
7233+
fn test_cp_absolute_paths_with_relative_source() {
7234+
// Test that cp works with absolute target when using relative source path
7235+
// This tests the path resolution logic in issue #9105
7236+
let (at, mut ucmd) = at_and_ucmd!();
7237+
7238+
at.mkdir("src");
7239+
at.touch("src/file.txt");
7240+
at.mkdir("dst");
7241+
7242+
ucmd.arg("-r").arg("src").arg("dst").succeeds();
7243+
7244+
assert!(at.file_exists("dst/src/file.txt"));
7245+
}
7246+
7247+
#[test]
7248+
#[cfg(unix)]
7249+
fn test_cp_absolute_source_and_target() {
7250+
// Test that cp works with both absolute source and target paths
7251+
// This tests issue #9105 fix: avoiding getcwd() for absolute paths
7252+
use std::fs;
7253+
use std::time::{SystemTime, UNIX_EPOCH};
7254+
7255+
let temp_dir = std::env::temp_dir();
7256+
let timestamp = SystemTime::now()
7257+
.duration_since(UNIX_EPOCH)
7258+
.unwrap()
7259+
.as_nanos();
7260+
let src_dir = temp_dir.join(format!("cp_test_src_{}", timestamp));
7261+
let dst_dir = temp_dir.join(format!("cp_test_dst_{}", timestamp));
7262+
7263+
// Create source directory with test files
7264+
fs::create_dir_all(&src_dir).unwrap();
7265+
fs::write(src_dir.join("file1.txt"), "content1").unwrap();
7266+
fs::write(src_dir.join("file2.txt"), "content2").unwrap();
7267+
7268+
// Perform copy with absolute paths
7269+
let (_at, mut ucmd) = at_and_ucmd!();
7270+
ucmd.arg("-r").arg(&src_dir).arg(&dst_dir).succeeds();
7271+
7272+
// Verify files were copied
7273+
assert!(dst_dir.exists());
7274+
assert!(dst_dir.join("file1.txt").exists());
7275+
assert!(dst_dir.join("file2.txt").exists());
7276+
7277+
// Clean up
7278+
let _ = fs::remove_dir_all(&src_dir);
7279+
let _ = fs::remove_dir_all(&dst_dir);
7280+
}

0 commit comments

Comments
 (0)