Skip to content

Commit 90c1b99

Browse files
committed
refactor(cp): remove unnecessary allocations
1 parent 18bfa3e commit 90c1b99

6 files changed

Lines changed: 86 additions & 169 deletions

File tree

src/uu/cp/src/copydir.rs

Lines changed: 47 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
//! Recursively copy the contents of a directory.
77
//!
88
//! See the [`copy_directory`] function for more information.
9-
#[cfg(windows)]
109
use std::borrow::Cow;
1110
use std::collections::{HashMap, HashSet};
1211
use std::convert::identity;
@@ -24,7 +23,7 @@ use uucore::fs::{
2423
use uucore::show;
2524
use uucore::translate;
2625
use uucore::uio_error;
27-
use walkdir::{DirEntry, WalkDir};
26+
use walkdir::WalkDir;
2827

2928
#[cfg(all(feature = "selinux", target_os = "linux"))]
3029
use crate::set_selinux_context;
@@ -44,7 +43,7 @@ struct DirNeedingPermissions {
4443
}
4544

4645
/// Ensure a Windows path starts with a `\\?`.
47-
#[cfg(target_os = "windows")]
46+
#[cfg(windows)]
4847
fn adjust_canonicalization(p: &Path) -> Cow<'_, Path> {
4948
// In some cases, \\? can be missing on some Windows paths. Add it at the
5049
// beginning unless the path is prefixed with a device namespace.
@@ -66,39 +65,6 @@ fn adjust_canonicalization(p: &Path) -> Cow<'_, Path> {
6665
}
6766
}
6867

69-
/// Get a descendant path relative to the given parent directory.
70-
///
71-
/// If `root_parent` is `None`, then this just returns the `path`
72-
/// itself. Otherwise, this function strips the parent prefix from the
73-
/// given `path`, leaving only the portion of the path relative to the
74-
/// parent.
75-
fn get_local_to_root_parent(
76-
path: &Path,
77-
root_parent: Option<&Path>,
78-
) -> Result<PathBuf, StripPrefixError> {
79-
match root_parent {
80-
Some(parent) => {
81-
// On Windows, some paths are starting with \\?
82-
// but not always, so, make sure that we are consistent for strip_prefix
83-
// See https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file for more info
84-
#[cfg(windows)]
85-
let (path, parent) = (
86-
adjust_canonicalization(path),
87-
adjust_canonicalization(parent),
88-
);
89-
let path = path.strip_prefix(parent)?;
90-
Ok(path.to_path_buf())
91-
}
92-
None => Ok(path.to_path_buf()),
93-
}
94-
}
95-
96-
/// Given an iterator, return all its items except the last.
97-
fn skip_last<T>(mut iter: impl Iterator<Item = T>) -> impl Iterator<Item = T> {
98-
let last = iter.next();
99-
iter.scan(last, Option::replace)
100-
}
101-
10268
/// Paths that are invariant throughout the traversal when copying a directory.
10369
struct Context<'a> {
10470
/// The current working directory at the time of starting the traversal.
@@ -120,11 +86,11 @@ struct Context<'a> {
12086
impl<'a> Context<'a> {
12187
fn new(root: &'a Path, target: &'a Path) -> io::Result<Self> {
12288
let current_dir = env::current_dir()?;
123-
let root_path = current_dir.join(root);
89+
let mut root_path = current_dir.join(root);
12490
let target_is_file = target.is_file();
12591
let root_parent =
12692
if target.exists() && !root.as_os_str().as_encoded_bytes().ends_with(b"/.") {
127-
root_path.parent().map(ToOwned::to_owned)
93+
root_path.pop().then_some(root_path)
12894
} else if root == Path::new(".") && target.is_dir() {
12995
// Special case: when copying current directory (.) to an existing directory,
13096
// we don't want to use the parent path as root_parent because we want to
@@ -179,12 +145,12 @@ impl<'a> Context<'a> {
179145
/// }
180146
/// ];
181147
/// ```
182-
struct Entry {
148+
struct Entry<'a> {
183149
/// The absolute path to file or directory to copy.
184150
source_absolute: PathBuf,
185151

186152
/// The relative path to file or directory to copy.
187-
source_relative: PathBuf,
153+
source_relative: &'a Path,
188154

189155
/// The path to the destination, relative to the target.
190156
local_to_target: PathBuf,
@@ -193,24 +159,44 @@ struct Entry {
193159
target_is_file: bool,
194160
}
195161

196-
impl Entry {
197-
fn new<A: AsRef<Path>>(
162+
impl<'a> Entry<'a> {
163+
fn new(
198164
context: &Context,
199-
source: A,
165+
source: &'a Path,
200166
no_target_dir: bool,
201167
) -> Result<Self, StripPrefixError> {
202-
let source = source.as_ref();
203-
let source_relative = source.to_path_buf();
204-
let source_absolute = context.current_dir.join(&source_relative);
205-
let mut descendant =
206-
get_local_to_root_parent(&source_absolute, context.root_parent.as_deref())?;
168+
let source_relative = source;
169+
let source_absolute = context.current_dir.join(source_relative);
170+
171+
// Get a descendant path relative to the given parent directory.
172+
// On Windows, some paths are starting with \\?
173+
// but not always, so, make sure that we are consistent for strip_prefix
174+
// See https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file for more info
175+
#[cfg(windows)]
176+
let (path, parent) = (
177+
adjust_canonicalization(&source_absolute),
178+
context
179+
.root_parent
180+
.as_deref()
181+
.map(|p| adjust_canonicalization(p)),
182+
);
183+
#[cfg(not(windows))]
184+
let (path, parent) = (
185+
Cow::from(&source_absolute),
186+
context.root_parent.as_deref().map(Cow::from),
187+
);
188+
let mut descendant = match parent {
189+
Some(parent) => path.strip_prefix(parent)?,
190+
None => &path,
191+
};
192+
207193
if no_target_dir {
208194
let source_is_dir = source.is_dir();
209195
if path_ends_with_terminator(context.target)
210196
&& source_is_dir
211197
&& !exists(context.target).is_ok_and(identity)
212198
{
213-
if let Err(e) = fs::create_dir_all(context.target) {
199+
if let Err(e) = fs::create_dir(context.target) {
214200
eprintln!(
215201
"{}",
216202
translate!("cp-error-failed-to-create-directory", "error" => e)
@@ -222,7 +208,7 @@ impl Entry {
222208
.next_back()
223209
.and_then(|stripped| descendant.strip_prefix(stripped).ok())
224210
{
225-
descendant = stripped.to_path_buf();
211+
descendant = stripped;
226212
}
227213
} else if context.root == Path::new(".") && context.target.is_dir() {
228214
// Special case: when copying current directory (.) to an existing directory,
@@ -233,7 +219,7 @@ impl Entry {
233219
if let Some(current_dir_name) = context.current_dir.file_name()
234220
&& let Ok(stripped) = descendant.strip_prefix(current_dir_name)
235221
{
236-
descendant = stripped.to_path_buf();
222+
descendant = stripped;
237223
}
238224
}
239225

@@ -288,7 +274,7 @@ fn copy_direntry(
288274
if options.verbose {
289275
println!(
290276
"{}",
291-
context_for(&entry.source_relative, &entry.local_to_target)
277+
context_for(entry.source_relative, &entry.local_to_target)
292278
);
293279
}
294280
Ok(true)
@@ -299,7 +285,7 @@ fn copy_direntry(
299285
if !source_is_dir
300286
&& let Err(err) = copy_file(
301287
progress_bar,
302-
&entry.source_relative,
288+
entry.source_relative,
303289
entry.local_to_target.as_path(),
304290
options,
305291
symlinked_files,
@@ -400,7 +386,7 @@ pub(crate) fn copy_directory(
400386
// a -> d/a
401387
// a/b -> d/a/b
402388
//
403-
let tmp = if options.parents {
389+
let target = if options.parents {
404390
if let Some(parent) = root.parent() {
405391
let new_target = target.join(parent);
406392
build_dir(&new_target, true)?;
@@ -415,31 +401,26 @@ pub(crate) fn copy_directory(
415401
println!("{} -> {}", x.display(), y.display());
416402
}
417403
}
418-
419-
new_target
404+
Cow::from(new_target)
420405
} else {
421-
target.to_path_buf()
406+
Cow::from(target)
422407
}
423408
} else {
424-
target.to_path_buf()
409+
Cow::from(target)
425410
};
426-
let target = tmp.as_path();
427411

428412
let preserve_hard_links = options.preserve_hard_links();
429413

430414
// Collect some paths here that are invariant during the traversal
431415
// of the given directory, like the current working directory and
432416
// the target directory.
433-
let context = match Context::new(root, target) {
417+
let context = match Context::new(root, &target) {
434418
Ok(c) => c,
435419
Err(e) => {
436420
return Err(translate!("cp-error-failed-get-current-dir", "error" => e).into());
437421
}
438422
};
439423

440-
// The directory we were in during the previous iteration
441-
let mut last_iter: Option<DirEntry> = None;
442-
443424
// Keep track of all directories we've created that need permission fixes
444425
let mut dirs_needing_permissions: Vec<DirNeedingPermissions> = Vec::new();
445426

@@ -481,13 +462,6 @@ pub(crate) fn copy_directory(
481462
// to prevent other users from accessing them before they're done.
482463
// We thus need to fix the permissions of each directory we copy
483464
// once it's contents are ready.
484-
// This "fixup" is implemented here in a memory-efficient manner.
485-
//
486-
// We detect iterations where we "walk up" the directory tree,
487-
// and fix permissions on all the directories we exited.
488-
// (Note that there can be more than one! We might step out of
489-
// `./a/b/c` into `./a/`, in which case we'll need to fix the
490-
// permissions of both `./a/b/c` and `./a/b`, in that order.)
491465
let is_dir_for_permissions =
492466
entry_is_dir_no_follow || (options.dereference && direntry_path.is_dir());
493467
if is_dir_for_permissions {
@@ -506,51 +480,10 @@ pub(crate) fn copy_directory(
506480
}
507481
// Add this directory to our list for permission fixing later
508482
dirs_needing_permissions.push(DirNeedingPermissions {
509-
source: entry.source_absolute.clone(),
510-
dest: entry.local_to_target.clone(),
483+
source: entry.source_absolute,
484+
dest: entry.local_to_target,
511485
was_created: created,
512486
});
513-
514-
// If true, last_iter is not a parent of this iter.
515-
// The means we just exited a directory.
516-
let went_up = if let Some(last_iter) = &last_iter {
517-
last_iter.path().strip_prefix(direntry_path).is_ok()
518-
} else {
519-
false
520-
};
521-
522-
if went_up {
523-
// Compute the "difference" between `last_iter` and `direntry`.
524-
// For example, if...
525-
// - last_iter = `a/b/c/d`
526-
// - direntry = `a/b`
527-
// then diff = `c/d`
528-
//
529-
// All the unwraps() here are unreachable.
530-
let last_iter = last_iter.as_ref().unwrap();
531-
let diff = last_iter.path().strip_prefix(direntry_path).unwrap();
532-
533-
// Fix permissions for every entry in `diff`, inside-out.
534-
// We skip the last directory (which will be `.`) because
535-
// its permissions will be fixed when we walk _out_ of it.
536-
// (at this point, we might not be done copying `.`!)
537-
for p in skip_last(diff.ancestors()) {
538-
let src = direntry_path.join(p);
539-
let entry = Entry::new(&context, &src, options.no_target_dir)?;
540-
541-
copy_attributes(
542-
&entry.source_absolute,
543-
&entry.local_to_target,
544-
&options.attributes,
545-
false,
546-
options.set_selinux_context,
547-
#[cfg(unix)]
548-
orig_umask,
549-
)?;
550-
}
551-
}
552-
553-
last_iter = Some(direntry);
554487
}
555488
}
556489

@@ -588,7 +521,7 @@ pub(crate) fn copy_directory(
588521
&src,
589522
y,
590523
&options.attributes,
591-
false,
524+
true,
592525
options.set_selinux_context,
593526
#[cfg(unix)]
594527
orig_umask,

0 commit comments

Comments
 (0)