Skip to main content

Module delete

Module delete 

Source
Expand description

The deleter: the half that cannot be undone.

§Why there is a plan

Every safety check happens while the Plan is being built, and removal executes the plan without re-deciding anything. That is what makes --dry-run honest rather than approximate: the thing printed is the same object the deleter consumes, so a preview cannot disagree with the run it previews.

§Resolving a path without following it

Proving a target is under the scan root means resolving .. and any symlinked ancestor, which is what fs::canonicalize does — except that canonicalising the target would also resolve the target itself, and a symlinked claim (Bazel’s bazel-*) must be unlinked as a link rather than followed to whatever it points at. So the parent is canonicalised and the final component is joined back on. Nothing can hide in the ancestry, and the leaf is left alone.

§The checks that fail toward “keep”

Tier two’s review (#588) found three bugs of the same shape: a check whose failure mode is silence, so an unreadable or unseen subtree reads as a cleared one. The deleter inherits that discipline. A directory it could not read is a failure, not an empty directory; a subtree it refused to enter leaves every ancestor standing, because the rmdir is only attempted when every child is known to be gone.

§The under-root check is not enough on its own, and why nothing is removed by name

The plan’s check is defence in depth against a malformed, stale or hostile path arriving from a caller, a config file or a scan of a tree someone else can write to. But what it proves, it proves about a path, and a path is a name that something else can re-point. A check like that is worth what it is worth at the moment of the unlink, not at the moment it ran — and in between sit a printed plan and a confirmation prompt.

So the removal never re-walks a target by name. It opens the scan root once and then descends by descriptor: every component is opened from its already-open parent with openat(fd, name, O_DIRECTORY | O_NOFOLLOW), and every removal is an unlinkat against the descriptor of the directory that holds the entry. A component swapped for a symlink fails the open with ELOOP rather than redirecting it, because the kernel resolves one name against one held descriptor and there is no path left for anything to re-point. “Under the root” becomes a property of how the syscall was issued.

cap-primitives supplies those calls. It is the same machinery cap-std is built from, and it is why this does not need libc and therefore does not need the unsafe this crate forbids: the descent this module always wanted turns out to be reachable in safe Rust. The root is opened once per batch rather than once per target, so the root itself cannot be swapped mid-run either.

One path still has to be resolved by name, and it cannot be avoided: the scan root has to be opened from somewhere. That makes it the most dangerous name in the program rather than an exempt one, because every descriptor descends from that handle — get it wrong and the whole batch is misdirected, not one target. So [open_root] opens the root’s final component with O_NOFOLLOW from its own parent, and then checks the descriptor’s (device, inode) against the pair recorded while the plan was built. The second check is the one that matters: a root renamed away and replaced by an ordinary directory on the same filesystem offers no symlink to refuse and crosses no boundary, so nothing about the name distinguishes it from the directory the planner validated. Only the inode does.

§Fan-out

unlink and rmdir are latency-bound rather than CPU-bound, so the pool is deliberately oversubscribed — the same conclusion the Node predecessor reached empirically. The unit of work is one target, not one directory: a sweep, which is the mode this exists for, has hundreds of targets, and per-target parallelism would need a join counter per directory to know when its rmdir is safe. Removing a single target is therefore single-threaded, as rm -rf is.

Structs§

Deleter
Removes what a Plan says to remove, and nothing else.
Failure
Something that went wrong. Collected rather than fatal.
Freeing
How far into one target a sweep has got.
Plan
A resolved, checked list of directories to remove.
PlanTarget
A target that survived every check, with its path resolved.
Planner
Builds a Plan under a fixed policy.
Refused
One directory that was left alone, and why.
Removal
What a removal did.
Removed
One target that was removed, in whole or in part.
Target
A directory offered for removal.

Enums§

Refusal
Why a directory was left where it is.
Step
What a removal reports while it is happening.

Functions§

confirm
Asks a yes/no question whose answer defaults to no.