Expand description
Generic single-tree safe-walk driver.
This module owns the recursive directory-walk skeleton that copy, chmod, and
rm previously each hand-coded. A tool supplies a WalkVisitor; the driver
drives the traversal:
- gated
read_entrieson the open hardened directory, - per child: authoritative-or-hinted
walk::filter_is_dir+walk::should_skip_entryfilter decision (against the child’sEntryCx::filter_path), thenwalk::preacquire_leaf_permitper the visitor’s policy, - acquire-then-spawn one task per non-skipped child, joining/folding via
join_and_fold(NOT batched: seewalk_dir_contentsfor why the permit is acquired and the task spawned in the same loop step), - in each task: authoritative
Dir::childclassification, then eitherWalkVisitor::visit_leaf(holding the permit) or — for a directory — drop the permit,WalkVisitor::dir_pre, recurse,WalkVisitor::dir_post.
§The single invariant home
The “drop the leaf permit before recursing into a directory” invariant — the
root cause of the hold-and-wait deadlock class (see walk::LeafPermit) —
lives in exactly one place: the directory branch of process_entry.
Leaves hold their permit across WalkVisitor::visit_leaf; the directory
branch drops it before any further work. No visitor ever hand-drops a leaf
permit, so the invariant cannot silently migrate back to N parallel sites.
§Cancellation safety
Spawned tasks must be 'static, and spawn_blocking work is not cancellable,
so every per-entry context is owned: EntryCx clones Arc<Dir> plus
owned OsString/PathBuf rather than borrowing, exactly as the existing
per-tool walks do. A dropped surrounding future (timeout, fail_early abort,
Ctrl-C) therefore can never leave a spawned task holding a dangling borrow.
§How copy maps onto this trait
copy is the reference single-tree visitor (rcp::copy::CopyVisitor); its
mapping shaped this trait.
CopyVisitor holds the run-constant state (dst_root, filter_base,
Settings, preserve::Settings, the opened top-level destination parent — the
source root needs no field, since each entry’s source path is its
EntryCx::real_path) and:
type Summary=copy::Summary.type DirContext= the destination parent for one level:{ dst_dir: Option<Arc<Dir>>, is_fresh: bool }(Nonedst = dry-run). This is how the single-tree driver carries copy’s second tree — each child reads its destination parent fromparent_ctxrather than the driver modeling two trees.WalkVisitor::root_dir_contextreturns the opened top-level destination (Some(dst)/None) with the initialis_fresh.type DirState={ dst_dir, dst_parent, dst_name, we_created, src_meta, is_root, base }— whatdir_postneeds to apply directory metadata (src_metais taken fromdir_pre’s classificationHandle, no extra stat), run empty-dir cleanup (dst_parent.rmdir_at(dst_name)), and--delete-prune, plus thebasecreate/unchanged contribution it folds with the children.visit_leafdispatches onkind:File→copy_file_fd,Symlink→copy_symlink_fd,Special→ skip-or-error. The pre-acquiredpermitis the open-files guardcopy_file_fdneeds; it is dropped for symlink/special.--dereferenceof a symlink-to-dir stays insidevisit_leaf: it drops the permit and calls the path-basedcopy()recursively (the one deliberately non-fd path), which the trait expresses fine —visit_leafis a plain async fn that may itself recurse without going through the driver.dir_prerunsresolve_dst_dir:DirResolution::Skip→DirAction::Skip(--ignore-existinghit a non-dir);Proceed{dir,..}→DirAction::Descendwhosediris the source dir (opened viasrc_parent.open_dir(name)),child_ctxcarries the resolveddst_dir+ childis_fresh, andstatecarries theDirState.dir_postreceives the children’s foldedResult: onOkit runs the--deleteprune (keep-set =processed.names()), empty-dir cleanup, andset_dir_metadata_fd(post-order); onErr(a non-fail-early child failure) it skips the destructive prune, still applies directory metadata, and returns the combined error — exactly ascopy_dir_contents’s tail did.on_skipmirrors copy’s inline filter-skip:report_skipin dry-run +skipped_summary_for(kind).permit_kind=OpenFile;want_permit= “hint isFile” (copy only pre-acquires for a regular-file hint — symlinks may deref to dirs, and DT_UNKNOWN might be a dir).
The delegated-subtree case (rlink handing copy an update-only/type-changed
subtree rooted below the original filter root) is carried by seeding the root
EntryCx::filter_path with the subtree’s logical base, so the filter still
matches at the entry’s true path while rel_path/real_path stay relative to
the delegated root.
The dry-run “directory” path (no destination dir, contents still traversed for
reporting) is just DirContext.dst_dir == None threaded through — the same
branch copy already has. No part of copy needs a trait shape this module does
not provide, which is why the trait stops here (no second-tree concept leaks
into the driver — that asymmetry is what keeps rlink on the substrate, not the
visitor; see docs/tocttou.md, “One shared traversal driver”).
Structs§
- EntryCx
- Owned per-entry context handed to every
WalkVisitormethod. - Processed
Children - The names of the non-skipped children the driver actually spawned for a directory, in enumeration order.
Enums§
- DirAction
- What a
WalkVisitor::dir_predecided to do with a directory entry.
Traits§
- Walk
Summary - A per-run summary accumulated by the walk.
- Walk
Visitor - A tool’s policy for a single-tree safe walk.
Functions§
- join_
and_ fold - Join an already-populated
JoinSetof per-child tasks and fold their summaries with fail-early / error-collection semantics — the shared join engine behind the directory walk. - process_
entry - Process one already-located entry: classify it authoritatively via
Dir::child, then dispatch. - walk_
dir_ contents - Walk the contents of an open hardened directory.
Type Aliases§
- DirPre
Result - The
ResultaWalkVisitor::dir_preproduces: aDirActionover the visitor’s three associated types, or itsOperationError. A named alias so the trait method’s signature stays readable.