@@ -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.
83110struct Context < ' a > {
84111 /// The current working directory at the time of starting the traversal.
@@ -162,17 +189,18 @@ struct Entry {
162189}
163190
164191impl 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.
218247fn 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