Skip to content

Commit 8e1965f

Browse files
committed
cp: efficient permission fixup
1 parent d9a2a9d commit 8e1965f

File tree

1 file changed

+109
-24
lines changed

1 file changed

+109
-24
lines changed

src/uu/cp/src/copydir.rs

Lines changed: 109 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,33 @@ fn get_local_to_root_parent(
7979
}
8080
}
8181

82+
/// Return the longest parent shared by two paths.
83+
/// If `a` and `b` to not share a parent, return `None`.
84+
fn common<A: AsRef<Path>, B: AsRef<Path>>(path_a: A, path_b: B) -> Option<PathBuf> {
85+
let path_a = path_a.as_ref().components();
86+
let path_b = path_b.as_ref().components();
87+
88+
let mut nonempty = false;
89+
let mut out = PathBuf::new();
90+
91+
for (a, b) in path_a.zip(path_b) {
92+
if a != b {
93+
break;
94+
}
95+
96+
out.push(a);
97+
nonempty = true;
98+
}
99+
100+
return nonempty.then_some(out);
101+
}
102+
103+
/// Given an iterator, return all its items except the last.
104+
fn skip_last<T>(mut iter: impl Iterator<Item = T>) -> impl Iterator<Item = T> {
105+
let last = iter.next();
106+
iter.scan(last, |state, item| std::mem::replace(state, Some(item)))
107+
}
108+
82109
/// Paths that are invariant throughout the traversal when copying a directory.
83110
struct Context<'a> {
84111
/// The current working directory at the time of starting the traversal.
@@ -162,17 +189,18 @@ struct Entry {
162189
}
163190

164191
impl Entry {
165-
fn new(
192+
fn new<A: AsRef<Path>>(
166193
context: &Context,
167-
direntry: &DirEntry,
194+
source: A,
168195
no_target_dir: bool,
169196
) -> Result<Self, StripPrefixError> {
170-
let source_relative = direntry.path().to_path_buf();
197+
let source = source.as_ref();
198+
let source_relative = source.to_path_buf();
171199
let source_absolute = context.current_dir.join(&source_relative);
172200
let mut descendant =
173201
get_local_to_root_parent(&source_absolute, context.root_parent.as_deref())?;
174202
if no_target_dir {
175-
let source_is_dir = direntry.path().is_dir();
203+
let source_is_dir = source.is_dir();
176204
if path_ends_with_terminator(context.target) && source_is_dir {
177205
if let Err(e) = std::fs::create_dir_all(context.target) {
178206
eprintln!("Failed to create directory: {e}");
@@ -213,6 +241,7 @@ where
213241
// `path.ends_with(".")` does not seem to work
214242
path.as_ref().display().to_string().ends_with("/.")
215243
}
244+
216245
#[allow(clippy::too_many_arguments)]
217246
/// Copy a single entry during a directory traversal.
218247
fn copy_direntry(
@@ -223,7 +252,6 @@ fn copy_direntry(
223252
preserve_hard_links: bool,
224253
copied_destinations: &HashSet<PathBuf>,
225254
copied_files: &mut HashMap<FileInformation, PathBuf>,
226-
dirs_with_attrs_to_fix: &mut Vec<(PathBuf, PathBuf)>,
227255
) -> CopyResult<()> {
228256
let Entry {
229257
source_absolute,
@@ -251,10 +279,6 @@ fn copy_direntry(
251279
if options.verbose {
252280
println!("{}", context_for(&source_relative, &local_to_target));
253281
}
254-
255-
// `build_dir` doesn't set fully set attributes,
256-
// we'll need to fix them later.
257-
dirs_with_attrs_to_fix.push((source_absolute, local_to_target));
258282
return Ok(());
259283
}
260284
}
@@ -408,15 +432,8 @@ pub(crate) fn copy_directory(
408432
Err(e) => return Err(format!("failed to get current directory {e}").into()),
409433
};
410434

411-
// We omit certain permissions when creating dirs
412-
// to prevent other uses from accessing them before they're done
413-
// (race condition).
414-
//
415-
// As such, we need to go back through the dirs we copied and
416-
// fix these permissions.
417-
//
418-
// This is a vec of (old_path, new_path)
419-
let mut dirs_with_attrs_to_fix: Vec<(PathBuf, PathBuf)> = Vec::new();
435+
// The directory we were in during the previous iteration
436+
let mut last_iter: Option<DirEntry> = None;
420437

421438
// Traverse the contents of the directory, copying each one.
422439
for direntry_result in WalkDir::new(root)
@@ -425,7 +442,8 @@ pub(crate) fn copy_directory(
425442
{
426443
match direntry_result {
427444
Ok(direntry) => {
428-
let entry = Entry::new(&context, &direntry, options.no_target_dir)?;
445+
let entry = Entry::new(&context, direntry.path(), options.no_target_dir)?;
446+
429447
copy_direntry(
430448
progress_bar,
431449
entry,
@@ -434,20 +452,87 @@ pub(crate) fn copy_directory(
434452
preserve_hard_links,
435453
copied_destinations,
436454
copied_files,
437-
&mut dirs_with_attrs_to_fix,
438455
)?;
456+
457+
// We omit certain permissions when creating directories
458+
// to prevent other uses from accessing them before they're done.
459+
// We thus need to fix the permissions of each directory we copy
460+
// once it's contents are ready.
461+
// This "fixup" is implemented here in a memory-efficient manner.
462+
//
463+
// Here, we detect iterations where we "walk up" the directory
464+
// tree, and fix permissions on all the directories we exited.
465+
// (Note that there can be more than one! We might step out of
466+
// `./a/b/c` into `./a/`, in which case we'll need to fix the
467+
// permissions of both `./a/b/c` and `./a/b`, in that order.)
468+
if direntry.file_type().is_dir() {
469+
// If true, last_iter is not a parent of this iter.
470+
// The means we just exited a directory.
471+
let went_up = if let Some(last_iter) = &last_iter {
472+
direntry.path().strip_prefix(&last_iter.path()).is_err()
473+
} else {
474+
false
475+
};
476+
477+
if went_up {
478+
// Compute the "difference" between `last_iter` and `direntry`.
479+
// For example, if...
480+
// - last_iter = `a/b/c/d`
481+
// - direntry = `a/b`
482+
// then diff = `c/d`
483+
let last_iter = last_iter.as_ref().unwrap();
484+
let common = common(direntry.path(), last_iter.path()).unwrap();
485+
let diff = last_iter.path().strip_prefix(&common).unwrap();
486+
487+
// Fix permissions for every entry in `diff`, inside-out.
488+
// We skip the last directory (which will be `.`) because
489+
// its permissions will be fixed when we walk _out_ of it.
490+
// (at this point, we might not be done copying `.`!)
491+
for p in skip_last(diff.ancestors()) {
492+
let src = common.join(p);
493+
let entry = Entry::new(&context, &src, options.no_target_dir)?;
494+
495+
copy_attributes(
496+
&entry.source_absolute,
497+
&entry.local_to_target,
498+
&options.attributes,
499+
)?;
500+
}
501+
}
502+
503+
last_iter = Some(direntry);
504+
}
439505
}
506+
440507
// Print an error message, but continue traversing the directory.
441508
Err(e) => show_error!("{}", e),
442509
}
443510
}
444511

445-
// Fix permissions for all directories we created
446-
for (src, tgt) in dirs_with_attrs_to_fix {
447-
copy_attributes(&src, &tgt, &options.attributes)?;
512+
// Handle final directory permission fixes.
513+
// This is almost the same as the permisson-fixing code above,
514+
// with minor differences (commented)
515+
if let Some(last_iter) = last_iter {
516+
let common = common(root, last_iter.path()).unwrap();
517+
let diff = last_iter.path().strip_prefix(&common).unwrap();
518+
519+
// Do _not_ skip `.` this time, since we know we're done.
520+
// This is where we fix the permissions of the top-level
521+
// directory we just copied.
522+
for p in diff.ancestors() {
523+
let src = common.join(p);
524+
let entry = Entry::new(&context, &src, options.no_target_dir)?;
525+
526+
copy_attributes(
527+
&entry.source_absolute,
528+
&entry.local_to_target,
529+
&options.attributes,
530+
)?;
531+
}
448532
}
449533

450-
// Copy the attributes from the root directory to the target directory.
534+
// Also fix permissions for parent directories,
535+
// if we were asked to create them.
451536
if options.parents {
452537
let dest = target.join(root.file_name().unwrap());
453538
for (x, y) in aligned_ancestors(root, dest.as_path()) {

0 commit comments

Comments
 (0)