Skip to content

Commit 1c62fbf

Browse files
committed
cp: fix subdir permissions
1 parent 6e9d4c8 commit 1c62fbf

File tree

1 file changed

+112
-11
lines changed

1 file changed

+112
-11
lines changed

src/uu/cp/src/copydir.rs

Lines changed: 112 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use uucore::fs::{
2323
use uucore::show;
2424
use uucore::show_error;
2525
use uucore::uio_error;
26-
use walkdir::WalkDir;
26+
use walkdir::{DirEntry, WalkDir};
2727

2828
use crate::{
2929
aligned_ancestors, context_for, copy_attributes, copy_file, copy_link, CopyResult, Error,
@@ -79,6 +79,12 @@ fn get_local_to_root_parent(
7979
}
8080
}
8181

82+
/// Given an iterator, return all its items except the last.
83+
fn skip_last<T>(mut iter: impl Iterator<Item = T>) -> impl Iterator<Item = T> {
84+
let last = iter.next();
85+
iter.scan(last, |state, item| std::mem::replace(state, Some(item)))
86+
}
87+
8288
/// Paths that are invariant throughout the traversal when copying a directory.
8389
struct Context<'a> {
8490
/// The current working directory at the time of starting the traversal.
@@ -248,7 +254,7 @@ fn copy_direntry(
248254
if target_is_file {
249255
return Err("cannot overwrite non-directory with directory".into());
250256
} else {
251-
build_dir(&local_to_target, false, options)?;
257+
build_dir(&local_to_target, false, options, Some(&source_absolute))?;
252258
if options.verbose {
253259
println!("{}", context_for(&source_relative, &local_to_target));
254260
}
@@ -373,7 +379,7 @@ pub(crate) fn copy_directory(
373379
let tmp = if options.parents {
374380
if let Some(parent) = root.parent() {
375381
let new_target = target.join(parent);
376-
build_dir(&new_target, true, options)?;
382+
build_dir(&new_target, true, options, None)?;
377383
if options.verbose {
378384
// For example, if copying file `a/b/c` and its parents
379385
// to directory `d/`, then print
@@ -405,6 +411,9 @@ pub(crate) fn copy_directory(
405411
Err(e) => return Err(format!("failed to get current directory {e}").into()),
406412
};
407413

414+
// The directory we were in during the previous iteration
415+
let mut last_iter: Option<DirEntry> = None;
416+
408417
// Traverse the contents of the directory, copying each one.
409418
for direntry_result in WalkDir::new(root)
410419
.same_file_system(options.one_file_system)
@@ -423,23 +432,93 @@ pub(crate) fn copy_directory(
423432
copied_destinations,
424433
copied_files,
425434
)?;
435+
436+
// We omit certain permissions when creating directories
437+
// to prevent other users from accessing them before they're done.
438+
// We thus need to fix the permissions of each directory we copy
439+
// once it's contents are ready.
440+
// This "fixup" is implemented here in a memory-efficient manner.
441+
//
442+
// We detect iterations where we "walk up" the directory tree,
443+
// and fix permissions on all the directories we exited.
444+
// (Note that there can be more than one! We might step out of
445+
// `./a/b/c` into `./a/`, in which case we'll need to fix the
446+
// permissions of both `./a/b/c` and `./a/b`, in that order.)
447+
if direntry.file_type().is_dir() {
448+
// If true, last_iter is not a parent of this iter.
449+
// The means we just exited a directory.
450+
let went_up = if let Some(last_iter) = &last_iter {
451+
last_iter.path().strip_prefix(direntry.path()).is_ok()
452+
} else {
453+
false
454+
};
455+
456+
if went_up {
457+
// Compute the "difference" between `last_iter` and `direntry`.
458+
// For example, if...
459+
// - last_iter = `a/b/c/d`
460+
// - direntry = `a/b`
461+
// then diff = `c/d`
462+
//
463+
// All the unwraps() here are unreachable.
464+
let last_iter = last_iter.as_ref().unwrap();
465+
let diff = last_iter.path().strip_prefix(direntry.path()).unwrap();
466+
467+
// Fix permissions for every entry in `diff`, inside-out.
468+
// We skip the last directory (which will be `.`) because
469+
// its permissions will be fixed when we walk _out_ of it.
470+
// (at this point, we might not be done copying `.`!)
471+
for p in skip_last(diff.ancestors()) {
472+
let src = direntry.path().join(p);
473+
let entry = Entry::new(&context, &src, options.no_target_dir)?;
474+
475+
copy_attributes(
476+
&entry.source_absolute,
477+
&entry.local_to_target,
478+
&options.attributes,
479+
)?;
480+
}
481+
}
482+
483+
last_iter = Some(direntry);
484+
}
426485
}
486+
427487
// Print an error message, but continue traversing the directory.
428488
Err(e) => show_error!("{}", e),
429489
}
430490
}
431491

432-
// Copy the attributes from the root directory to the target directory.
492+
// Handle final directory permission fixes.
493+
// This is almost the same as the permission-fixing code above,
494+
// with minor differences (commented)
495+
if let Some(last_iter) = last_iter {
496+
let diff = last_iter.path().strip_prefix(root).unwrap();
497+
498+
// Do _not_ skip `.` this time, since we know we're done.
499+
// This is where we fix the permissions of the top-level
500+
// directory we just copied.
501+
for p in diff.ancestors() {
502+
let src = root.join(p);
503+
let entry = Entry::new(&context, &src, options.no_target_dir)?;
504+
505+
copy_attributes(
506+
&entry.source_absolute,
507+
&entry.local_to_target,
508+
&options.attributes,
509+
)?;
510+
}
511+
}
512+
513+
// Also fix permissions for parent directories,
514+
// if we were asked to create them.
433515
if options.parents {
434516
let dest = target.join(root.file_name().unwrap());
435-
copy_attributes(root, dest.as_path(), &options.attributes)?;
436517
for (x, y) in aligned_ancestors(root, dest.as_path()) {
437518
if let Ok(src) = canonicalize(x, MissingHandling::Normal, ResolveMode::Physical) {
438519
copy_attributes(&src, y, &options.attributes)?;
439520
}
440521
}
441-
} else {
442-
copy_attributes(root, target, &options.attributes)?;
443522
}
444523

445524
Ok(())
@@ -472,13 +551,21 @@ pub fn path_has_prefix(p1: &Path, p2: &Path) -> io::Result<bool> {
472551
/// Builds a directory at the specified path with the given options.
473552
///
474553
/// # Notes
475-
/// - It excludes certain permissions if ownership or special mode bits could
476-
/// potentially change.
554+
/// - If `copy_attributes_from` is `Some`, the new directory's attributes will be
555+
/// copied from the provided file. Otherwise, the new directory will have the default
556+
/// attributes for the current user.
557+
/// - This method excludes certain permissions if ownership or special mode bits could
558+
/// potentially change. (See `test_dir_perm_race_with_preserve_mode_and_ownership``)
477559
/// - The `recursive` flag determines whether parent directories should be created
478560
/// if they do not already exist.
479561
// we need to allow unused_variable since `options` might be unused in non unix systems
480562
#[allow(unused_variables)]
481-
fn build_dir(path: &PathBuf, recursive: bool, options: &Options) -> CopyResult<()> {
563+
fn build_dir(
564+
path: &PathBuf,
565+
recursive: bool,
566+
options: &Options,
567+
copy_attributes_from: Option<&Path>,
568+
) -> CopyResult<()> {
482569
let mut builder = fs::DirBuilder::new();
483570
builder.recursive(recursive);
484571

@@ -487,6 +574,9 @@ fn build_dir(path: &PathBuf, recursive: bool, options: &Options) -> CopyResult<(
487574
// could potentially change.
488575
#[cfg(unix)]
489576
{
577+
use crate::Preserve;
578+
use std::os::unix::fs::PermissionsExt;
579+
490580
// we need to allow trivial casts here because some systems like linux have u32 constants in
491581
// in libc while others don't.
492582
#[allow(clippy::unnecessary_cast)]
@@ -498,7 +588,18 @@ fn build_dir(path: &PathBuf, recursive: bool, options: &Options) -> CopyResult<(
498588
} else {
499589
0
500590
} as u32;
501-
excluded_perms |= uucore::mode::get_umask();
591+
592+
let umask = if copy_attributes_from.is_some()
593+
&& matches!(options.attributes.mode, Preserve::Yes { .. })
594+
{
595+
!fs::symlink_metadata(copy_attributes_from.unwrap())?
596+
.permissions()
597+
.mode()
598+
} else {
599+
uucore::mode::get_umask()
600+
};
601+
602+
excluded_perms |= umask;
502603
let mode = !excluded_perms & 0o777; //use only the last three octet bits
503604
std::os::unix::fs::DirBuilderExt::mode(&mut builder, mode);
504605
}

0 commit comments

Comments
 (0)