Glob walk: a drive-root base no longer drops the pattern, and three ways a failure could lose a match - #575
Open
ronleizrowice-ant wants to merge 8 commits into
Open
ronleizrowice-ant wants to merge 8 commits into
ronleizrowice-ant wants to merge 8 commits into
Conversation
…or on one path.dirname keeps the separator of a root it returns, so the base of a pattern whose first wildcard sits right below one is 'C:/', or '//server/share/' on a share. Split into path components to seed the walk, either ends in an empty name no position can consume: the automaton starts nowhere, the pattern matches nothing, and the entry is dropped with no error and no warning. On Windows that is every denyRead, denyWrite, allowRead and allowWrite glob of the shape C:\Users*\**\.env. A base now never carries a separator, and one that is a filesystem root -- '/', a drive root, the root of a UNC share -- is refused and warned about the way the POSIX root already was, in the walk and in the manager's glob-pattern warnings, through one predicate so that the two cannot drift.
…ough a link A pattern that cannot be read one path component at a time is matched against whole paths, and no directory is listed through a symlink under it. A link leading out of the walk's tree was dropped: what the pattern matches in there is found under no other name, so the deny lost it. Such a link is now recorded as a place the walk could not enumerate, which the read-deny expansion denies whole and binds nothing back beneath. A link within the tree still loses nothing, since every directory there is listed under its own name. The link the walk follows is now named where it is followed, which is the one place that knows it: what is found through it is reported where it really lives, so the warning about a mount outside the pattern's base cannot name it.
The positions a directory had been listed for were marked before the listing and never rolled back, so a listing that failed answered for every later name that led there: a transient EMFILE, or a real path too long to name, dropped every match beneath the directory for the rest of the walk. A failure is uncached again, with one record per real directory, so the directory is named once in `unlisted` however many names fail on it, and a retry costs one listing per name that leads to it -- each of them an entry of a directory that is itself listed once per position. A filesystem call also no longer answers from a second name for anything but a path too long to be one. Every other errno belongs to the object rather than to the name, and the shorter name crosses links a sandboxed command owns: EACCES on the real path followed by ENOENT on the short one read as "nothing is there", which is the fail-open the unlistable and uninspectable branches exist to prevent.
Both of globPositions' regular expressions are compiled inside the try that guards the automaton, and a pattern that compiles into no regular expression at all is raised under its own name rather than thrown from wherever the sandbox happened to be setting up. A pattern the automaton cannot be built for -- including one so long that building it would overflow the stack, which a bound on the piece count now refuses -- falls back to matching whole real paths with a warning that says so, rather than quietly covering less: on a deny path that fallback also stops the walk descending symlinked directories. Beside that: the directory-form automaton is built only for the caller that asks for it, the positions are no longer sorted on every step for an order nothing reads, and the state written back into the automaton is held rather than cast.
… the separator Nothing in the tree held the walk's automaton and globToRegex together, though they are the two halves of one rule and are edited by different hands. A seeded, bounded property test compares them over generated trees and patterns: what the walk returns must be exactly what the regular expression matches beneath the pattern's base. It takes about 30ms. Also: a bracket range that holds the separator ([+-9]) joins the set that does ([s/]), and the fixture that builds a path near PATH_MAX no longer spins for ever when the temporary directory is exactly the length that leaves it nothing to add.
A link back up the tree is not descended, so it denies what it reaches only when the link is itself a match; the README said it always did. It now also says what a pattern that cannot be split does with a link that leads out of the starting directory.
Open
The spy type is not disposable under the type packages the test typecheck runs with, so `using spy = spyOn(...)` does not compile there. Each becomes a const restored in a finally, as the other suites do it, with the same scope the declaration had.
…base Two things the earlier commits on this branch got wrong. A pattern that cannot be read one component at a time denies whole a link that leads out of its starting directory, since nothing in there is listed under any name. "Out of the starting directory" included every ANCESTOR of it, and the test that leaves a link leading up alone came after that branch, where such a pattern never reaches it. So with an unsplittable deny glob, one `ln -s ../.. x` in the tree put a tmpfs over the directory holding the project, with nothing bound back beneath it, for every later command. A link that leads back up to the starting directory or above it is now left alone, as it is for a pattern that can be followed. The refusal of a filesystem root as a base was written for `/`, which is refused so that a whole filesystem is not listed. It also matched a drive root and a share root, so every Windows pattern whose only literal directory is one (`\\server\share\*.pem`, `D:\**\*.key`) matched nothing and was not enforced, where the last release walks and enforces them. Only `/` and a pattern with no literal directory are refused now. A base never carries a trailing separator; a drive root is handed to the filesystem as `C:/`, since `C:` alone is that drive's current directory. The README also says what is still not covered: a link whose target cannot be looked at, and that the pattern would continue through rather than match, is not followed and nothing is denied for it.
This was referenced Sep 25, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The walk behind a read-deny glob reads its pattern as an automaton, one path component at a time (#564). Reading it again turned up one base it cannot start from and three ways a failure loses a match instead of reporting it. The first drops a whole configuration entry on Windows, in silence.
What was wrong
path.dirnamekeeps the separator of a root, so the base ofC:\Users*\**\.envisC:/and of\\server\share\x*\yis//server/share/. Split into path components to seed the automaton, either ends in an empty name no position can consume: the walk starts nowhere, nothing matches, and the entry is dropped with no error and no warning. That is everydenyRead,denyWrite,allowReadandallowWriteglob of that shape on Windows.EMFILE, or a real path too long to name, answered for every later name that led there and dropped every match beneath it.EACCESon the real path thenENOENTon the short one reads as "nothing is there" -- no match, nothing unlistable, no deny.RangeError.Change
A base never carries a trailing separator, so a Windows pattern whose wildcard sits in the component right below a drive root or a share root (
C:\Users*\**\.env,\\server\share\x*\y) is now walked instead of matching nothing; a drive root is handed to the filesystem asC:/, sinceC:alone is the drive's current directory. Only/and a pattern with no literal directory at all are refused, as before, in the walk and in the manager's glob warnings through one predicate; a**right below a drive or share root lists that volume. An earlier head of this branch refused a drive root and a share root as it refuses/, which also dropped the shapes the last release enforces (\\server\share\*.pem,D:\**\*.key); that refusal is gone. A failure is uncached again: one record per real directory, named once inunlisted, retried under each name that leads to it. A call answers from a second name only for a path too long to be one, and logs which it used. A link out of the tree that an unsplittable pattern cannot be followed through is denied whole, except one that leads back up to the starting directory or above it: such a link is left alone, as it is for a pattern that can be followed, because what it leads to holds the starting directory itself, and denying that whole would hide it, everything beside it and every path bound back beneath it, over one link anything able to write the tree can make; the links the walk does follow are named where it follows them. Both regular expressions are compiled inside the try that guards the automaton: a pattern that compiles into none is raised under its own name, one too long to build is refused at a bound on the piece count, and either fallback warns. Cleanups: the directory-form automaton is built only for the caller that asks, positions are not sorted for an order nothing reads, the automaton's state is held rather than cast.Who sees a difference
**there lists the whole volume, atinitialize(); a rule of that shape is better off naming the directories it means (C:\Users\*\...starts fromC:\Users). For a drive-root pattern on the working directory's own drive,mainlisted the current directory and spelled the results as if they were under the root, so nothing was stamped; handing the root over asC:/puts that right as well.denyReadglob now mounts over the directory a link out of the tree leads to, and binds nothing back beneath it.Not done here
globToRegexitself: globToRegex: negate bracket sets, and compile from tokens so no path text can become a wildcard #573 rewrites it. This branch keeps towalkGlobPattern,globPositionsandglobPatternBaseDir; the two meet in one README line and in the piece bound, which counts whatglobPiecesreturns either way. globToRegex: negate bracket sets, and compile from tokens so no path text can become a wildcard #573 also drops one of the three shapes the README calls unsplittable, so that clause is left to it.**pattern is any directory with such a link in it, the project root included. A link of that kind that is itself a match is still denied under its own spelling. The README says so.C:withpath's Windows semantics spied in; a share root is covered by the pure functions only, since a leading//does not survive path normalisation on POSIX.Testing
tsc --noEmit,eslintandprettier --checkclean on the changed files; fullnpm teston Linux matches a clean checkout ofmainon the same machine. Againstmain's sources, with only the new predicate carried over, all five new behaviour tests fail. The three added last (a link that leads up is in neither the matches nor the unlisted directories, and no mount is put over what holds the tree; a drive root and a share root are not refused and their bases carry no separator; the walk from a drive-shaped base finds its matches) fail at the head before them and fail again with each fix taken back out.Live, under bubblewrap: with
denyRead: ["<root>/proj/**/?[*].pem"]andproj/away -> outside,mainreadsoutside/b].pemin full inside the sandbox where this branch finds an empty directory, and a match inside the tree stays masked in both. The property test compares the walk withglobToRegexover four generated trees and 500 patterns in about 30ms.