fix(fs): finish a cross-mount copy around an entry it cannot copy, then report them - #422
mutewinter wants to merge 2 commits into
Conversation
…en report them A recursive cp from one mount into another walks the tree entry by entry, and one entry the destination refuses ended the whole walk with only that entry named. The ordinary case is a symlink, which ReadWriteFs and OverlayFs refuse to create unless allowSymlinks is set, so a tree holding a single link failed on it with everything after it left uncopied. The walk now continues past an entry it cannot copy and throws once at the end, naming the entries it could not copy (capped at ten), the way GNU cp reports each failed entry and exits 1 when it is done. A copy within one mount is that mount's own and is unchanged.
|
@mutewinter is attempting to deploy a commit to the Vercel Labs Team on Vercel. A member of the Team first needs to authorize it. |
cp -R of any attached folder holding a symlink stopped at the link with EPERM: operation not permitted, symlink ..., everything after it uncopied, and the errno was the one the attached-folders prompt told the agent to read as macOS refusing the folder. Upstream's MountableFs ends a cross-mount walk at the first entry the destination refuses; fixed there as vercel-labs/just-bash#422 (the walk finishes and reports the failed entries together, as GNU cp does) and carried as the third part of the 3.4.1 patch until it ships, guarded by create-bash-env-cp.test.ts. The prompt's EPERM rule narrows to reads and listings, since the destination still reports the errno for the links it left out. A subclass overriding cp at our layer was built first and rejected: it would shadow any upstream change to the copy and has no removal trigger; the decision doc records it.
🤖 auto-maintain reviewAutomated, advisory triage for
Review panel: 🟡 medium highest severity just-bash maintainer code review: 🟡 medium
General code review: 🟡 medium
Adversarial security: 🟡 medium
Adversarial security (second opinion): 🟡 medium
Standard Bash and host portability: 🟢 low
Posted by auto-maintain. This automated code review is advisory; a human maintainer makes the call. |
Every failure message was retained until the walk finished, so a tree the destination refuses most of held one string per refused entry against a cap that only shaped the message. Only the first ten are kept now; the rest are counted. Each named failure leads with the source entry rather than whatever path the destination's error embedded, and a directory that failed before its children were reached is named with its contents, since they are missing with it.
|
Addressed the three points from the automated review in 39b0903:
New test for the skipped-directory case; the two existing ones updated for the message shape. |
Upstream's review on vercel-labs/just-bash#422 asked for three things, all now on the branch and carried here: only the first ten failures are kept while the walk runs (the rest are counted), each named failure leads with the source entry rather than the destination's own path, and a directory that failed before its children were reached is named with its contents. Guard snapshot and the doc examples follow the new message shape.
Problem
A recursive copy from one mount into another ends at the first entry the destination refuses, and everything after it is left uncopied. The ordinary case is a symlink:
ReadWriteFsandOverlayFsrefuse to create one unlessallowSymlinksis set, so any tree with a link in it (a checked-out repository, anode_modules) fails this way. For an agent this reads as the folder being off limits rather than one entry being skipped, and it is easy to misattribute to the host: the errno is the one macOS raises when it denies an app a folder.Cause
crossMountCopyrecurses per child with no handling between entries:The first rejected promise unwinds the whole walk. The existing cross-mount tests copy between two
InMemoryFs, which create symlinks, so no entry ever fails in them.Fix
The walk continues past an entry it cannot copy and, once the tree is done, throws one error naming the entries it could not copy (capped at ten, then
and N more), the way GNUcpreports each failed entry and exits 1 at the end. Thecpcommand needs no change: it prints the error ascp: cannot copy '<src>': copied all but 2 entries: ...and exits 1.Scope
Unchanged: a copy within one mount is delegated to that filesystem's own
cpas before. A failure on the source operand itself (unreadable, missing) still throws directly. A cross-mountmvthat could not copy everything leaves the source in place, as it did when the copy aborted.Not addressed, deliberately: a symlink policy for
cp(-L,--no-dereference), which is a command-level feature rather than this bug; a structured error type carrying the failures, which nothing consumes yet; per-entry lines on stderr, since the filesystem layer has no stderr and the command prints one line per source operand.Tests
Two cases in
mountable-fs.test.ts, using anInMemoryFssubclass whosesymlink()throwsEPERM(the policyReadWriteFsandOverlayFsdefault to). The first asserts the files before, beside, and after two links (one nested) all arrive, the links do not, and the message names both in traversal order. The second asserts twelve refused links report ten andand 2 more. They do not exercise a realReadWriteFsdestination or thecpcommand's rendering; both are covered in the consumer suite this was found in, not here.vitest run src/fs/ src/commands/cp/ src/commands/mv/: 33 files, 1197 passed, 1 skipped (pre-existing).tsc --noEmitin my checkout reports errors insrc/commands/js-exec/run-runtime.tsonly, arunmodule not installed here; pre-existing and untouched by this change.Authored with Claude Opus 5