@@ -79,6 +79,33 @@ fn get_local_to_root_parent(
79
79
}
80
80
}
81
81
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
+
82
109
/// Paths that are invariant throughout the traversal when copying a directory.
83
110
struct Context < ' a > {
84
111
/// The current working directory at the time of starting the traversal.
@@ -162,17 +189,18 @@ struct Entry {
162
189
}
163
190
164
191
impl Entry {
165
- fn new (
192
+ fn new < A : AsRef < Path > > (
166
193
context : & Context ,
167
- direntry : & DirEntry ,
194
+ source : A ,
168
195
no_target_dir : bool ,
169
196
) -> 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 ( ) ;
171
199
let source_absolute = context. current_dir . join ( & source_relative) ;
172
200
let mut descendant =
173
201
get_local_to_root_parent ( & source_absolute, context. root_parent . as_deref ( ) ) ?;
174
202
if no_target_dir {
175
- let source_is_dir = direntry . path ( ) . is_dir ( ) ;
203
+ let source_is_dir = source . is_dir ( ) ;
176
204
if path_ends_with_terminator ( context. target ) && source_is_dir {
177
205
if let Err ( e) = std:: fs:: create_dir_all ( context. target ) {
178
206
eprintln ! ( "Failed to create directory: {e}" ) ;
@@ -213,6 +241,7 @@ where
213
241
// `path.ends_with(".")` does not seem to work
214
242
path. as_ref ( ) . display ( ) . to_string ( ) . ends_with ( "/." )
215
243
}
244
+
216
245
#[ allow( clippy:: too_many_arguments) ]
217
246
/// Copy a single entry during a directory traversal.
218
247
fn copy_direntry (
@@ -223,7 +252,6 @@ fn copy_direntry(
223
252
preserve_hard_links : bool ,
224
253
copied_destinations : & HashSet < PathBuf > ,
225
254
copied_files : & mut HashMap < FileInformation , PathBuf > ,
226
- dirs_with_attrs_to_fix : & mut Vec < ( PathBuf , PathBuf ) > ,
227
255
) -> CopyResult < ( ) > {
228
256
let Entry {
229
257
source_absolute,
@@ -251,10 +279,6 @@ fn copy_direntry(
251
279
if options. verbose {
252
280
println ! ( "{}" , context_for( & source_relative, & local_to_target) ) ;
253
281
}
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) ) ;
258
282
return Ok ( ( ) ) ;
259
283
}
260
284
}
@@ -408,15 +432,8 @@ pub(crate) fn copy_directory(
408
432
Err ( e) => return Err ( format ! ( "failed to get current directory {e}" ) . into ( ) ) ,
409
433
} ;
410
434
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 ;
420
437
421
438
// Traverse the contents of the directory, copying each one.
422
439
for direntry_result in WalkDir :: new ( root)
@@ -425,7 +442,8 @@ pub(crate) fn copy_directory(
425
442
{
426
443
match direntry_result {
427
444
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
+
429
447
copy_direntry (
430
448
progress_bar,
431
449
entry,
@@ -434,20 +452,87 @@ pub(crate) fn copy_directory(
434
452
preserve_hard_links,
435
453
copied_destinations,
436
454
copied_files,
437
- & mut dirs_with_attrs_to_fix,
438
455
) ?;
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
+ }
439
505
}
506
+
440
507
// Print an error message, but continue traversing the directory.
441
508
Err ( e) => show_error ! ( "{}" , e) ,
442
509
}
443
510
}
444
511
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
+ }
448
532
}
449
533
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.
451
536
if options. parents {
452
537
let dest = target. join ( root. file_name ( ) . unwrap ( ) ) ;
453
538
for ( x, y) in aligned_ancestors ( root, dest. as_path ( ) ) {
0 commit comments