Files
node/test/parallel/test-glob-syntax.js
avivkeller a8f2bf007e fs: implement glob natively
Signed-off-by: avivkeller <me@aviv.sh>
PR-URL: https://github.com/nodejs/node/pull/65392
Refs: https://openjs-foundation.slack.com/archives/C019Y2T6STH/p1787069174254509
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
2026-09-11 13:15:27 -04:00

490 lines
16 KiB
JavaScript

'use strict';
const common = require('../common');
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const tmpdir = require('../common/tmpdir');
const cases = /* [path, pattern, matches] */ [
// Path separators.
['a/b', 'a/b', true],
['a/b', 'a//b', true], // Repeated separators collapse.
['a//b', 'a/b', true],
['a/', 'a', true], // A trailing separator on the path is ignored.
['a/b', 'a/b/', false],
['', '', true],
['a', '', false],
['', '*', false],
['/a', '/a', true],
['a', '/a', false],
// `*` matches any run of characters within one segment.
['abc', '*', true],
['a/b', '*', false],
['a/b', '*/*', true],
['abc', 'a*c', true],
['ac', 'a*c', true],
['a/b', 'a*b', false],
['a.txt', '*.txt', true],
['a.c', '*.*', true],
['abc', '*.*', false],
// `?` matches exactly one character within one segment.
['abc', 'a?c', true],
['ac', 'a?c', false],
['a/c', 'a?c', false],
// Bracket expressions.
['a', '[abc]', true],
['d', '[abc]', false],
['b', '[a-c]', true],
['d', '[!abc]', true],
['d', '[^abc]', true],
['a', '[!abc]', false],
[']', '[]]', true], // A `]` first in the set is a literal `]`.
['a', '[]a]', true],
['-', '[-a]', true],
['-', '[a-]', true],
['[a', '[a', true], // An unterminated `[` is a literal `[`.
['a', '[a', false],
['/', '[/]', false], // A set never matches a separator.
['*', '[*]', true], // Sets are how magic characters are escaped.
['x', '[*]', false],
['?', '[?]', true],
['{', '[{]', true],
// POSIX classes cover the whole of Unicode.
['5', '[[:digit:]]', true],
['a', '[[:digit:]]', false],
['a', '[![:digit:]]', true],
['5', '[![:digit:]]', false],
['a', '[[:alpha:]]', true],
['é', '[[:alpha:]]', true],
['é', '[a-zA-Z]', false],
['_', '[[:word:]]', true],
['A', '[[:upper:]]', true],
['a', '[[:foo:]]', false], // An unknown class is not a class.
['a', '[[=a=]]', false], // Equivalence classes are unsupported.
['ch', '[[.ch.]]', false], // So are collating symbols.
// A `**` alone in a segment matches zero or more segments.
['a/b/c', 'a/**/c', true],
['a/c', 'a/**/c', true],
['a/x/y/c', 'a/**/c', true],
['a/b/c/d', 'a/**', true],
['a/b/c', 'a/**/**/c', true],
['a/b/c', '**/c', true],
['a/b', '**', true],
// Anywhere else, `**` is just a `*`.
['a/xb', 'a/**b', true],
['a/xb', 'a/*b', true],
['a/x/b', 'a/**b', false],
// And it does not descend into dot directories.
['a/.b/c', 'a/**/c', false],
['.a/b', '**', false],
// Extended globs.
['abc', '@(abc|def)', true],
['def', '@(abc|def)', true],
['abcdef', '@(abc|def)', false],
['ac', 'a?(b)c', true],
['abc', 'a?(b)c', true],
['abbc', 'a?(b)c', false],
['ac', 'a*(b)c', true],
['abbc', 'a*(b)c', true],
['ac', 'a+(b)c', false],
['abc', 'a+(b)c', true],
['abbc', 'a+(b)c', true],
['abd', '!(abc)', true],
['abc', '!(abc)', false],
['abc', '*(a|@(b|c))', true], // They nest.
['index.ts', '*.@(js|ts)', true],
['index.css', '!(*.js)', true],
['ab', '!(a)*', false],
// Brace expansion.
['a', '{a,b}', true],
['c', '{a,b}', false],
['abd', 'a{b,c}d', true],
['a', 'a{,b}', true],
['ab', 'a{,b}', true],
['ab', 'a{b,{c,d}}', true], // Braces nest.
['ac', 'a{b,{c,d}}', true],
['ad', 'a{b,{c,d}}', true],
['a1b', 'a{1..3}b', true], // Numeric sequences.
['a4b', 'a{1..3}b', false],
['a-1b', 'a{-1..1}b', true],
['a01b', 'a{01..03}b', true], // Zero padding is kept.
['a1b', 'a{1..9..3}b', true], // Sequences may take a step.
['a4b', 'a{1..9..3}b', true],
['a5b', 'a{1..9..3}b', false],
['ac', 'a{b..d}', true], // Alphabetic sequences.
['a{b}c', 'a{b}c', true], // No comma and no sequence means no expansion.
['abc', 'a{b}c', false],
['{}', '{}', true],
// A leading dot is only matched by a literal dot.
['.hidden', '*', false],
['.hidden', '.*', true],
['.a', '?a', false],
['.a', '[.]a', true],
['a/.b/c', 'a/*/c', false],
['a/.b/c', 'a/.*/c', true],
// `.` and `..` are never matched by a wildcard.
['.', '*', false],
['..', '*', false],
['.', '.', true],
// There is no escape character: a backslash is a path separator.
['a/b', 'a\\b', true],
['*', '\\*', false],
['a*b', 'a\\*b', false],
// A leading `!` does not negate and a leading `#` does not comment.
['!a', '!a', true],
['a', '!a', false],
['#a', '#a', true],
// Patterns are normalized for walking a file system.
['a/c', 'a/b/../c', true],
['a/b', 'a/./b', true],
];
// These are the odd cases that minimatch explicitly tests, so
// we ensure compatiility.
//
// See https://github.com/isaacs/minimatch/blob/main/test/patterns.js
const minimatchCases = [
// Runs of stars and question marks collapse
['abc', 'a***c', true],
['abc', 'a*****?c', true],
['abc', '?*****??', true],
['abc', '*****??', true],
['abc', '?***?****c', true],
['abc', '?***?****?', true],
['abc', '*******c', true],
['abc', '*******?', true],
['abc', '??**********?****?', false],
['abc', '*c*?**', false],
['abc', 'a********???*******', false],
['abcdecdhjk', 'a*cd**?**??k', true],
['abcdecdhjk', 'a**?**cd**?**??***k**', true],
['a/b/b', 'a/*/b', true],
// Bracket expression edge cases
['[', '[', true],
['[abc', '[*', true],
['a', '[]', false],
['p', '[a-z]', true],
[']', '[]-]', true],
['-', '[-abc]', true],
['-', '[abc-]', true],
['[', '[[]', true],
['a]b', 'a[]]*', true],
['a[]b', 'a[]*', true],
['a[[]b', 'a[[]*', true],
['a', '[z-a]', false],
['fffff', '[z-af]*', true],
['fffff', '[f-gz-a]*', true],
['a', '[a-0][a-Ā]', false],
['a', '[a-b-c]', true],
['[!ab', '[!a*', true],
['[ab', '[!a*', false],
['[#ab', '[#a*', true],
// POSIX classes
['åéîøü', '[[:alpha:]][[:alpha:]][[:alpha:]][[:alpha:]][[:alpha:]]', true],
['aeiou', '[[:alpha:]][[:alpha:]][[:alpha:]][[:alpha:]][[:alpha:]]', true],
['0f7fa', '[[:alpha:]][[:alpha:]][[:alpha:]][[:alpha:]][[:alpha:]]', false],
['aeiou', '[[:ascii:]][[:ascii:]][[:ascii:]][[:ascii:]][[:ascii:]]', true],
['åéîøü', '[[:ascii:]][[:ascii:]][[:ascii:]][[:ascii:]][[:ascii:]]', false],
['0f7fa', '[[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]][[:xdigit:]]',
true],
['99999', '[[:xdigit:]][[:xdigit:]]???', true],
['0f7fa', '[[:graph:]]f*', true],
['fffff', '[[:graph:][:digit:]]f*', true],
['åéîøü', '[[:alnum:]][[:alnum:]][[:alnum:]][[:alnum:]][[:alnum:]]', true],
['a', '[a-[:alpha:]*]', false],
// Extended globs
['foo.bar', '*.!(js)', true],
['foo.', '*.!(js)', true],
['boo.js.boo', '*.!(js)', true],
['foo.js.js', '*.!(js)', true],
['foo.js', '*.!(js)', false],
['blar.js', '*.!(js)', false],
['ac', '+(a)!(b)+(c)', true],
['acc', '+(a)!(b)+(c)', true],
['adc', '+(a)!(b)+(c)', true],
['abc', '+(a)!(b)+(c)', false],
['bac', '+(a)!(b)+(c)', false],
['a/b/c/bar/x', 'a/b/*/!(bar)/*', false],
['a/b/c/baz/x', 'a/b/*/!(bar)/*', true],
['ax', 'a?(b*)', false],
['ax', '?(a*|b)', true],
['x-ab', '?(x-!(y)|z)b', true],
['zb', '?(x-!(y)|z)b', true],
['x-a', '?(x-!(y)|z)', true],
['fool', '@(foo)*', true],
['oof', '@(foo)*', false],
['a/b', '*(a/b)', false],
['aa', '*(?)', true],
['aa.', '+(?)', true],
['ab', '+(a|?)', true],
['b.a', '+(a|!(b))', true],
['.aa', '+(.|a|!(b))', true],
['aa', '+(a|.)', true],
['x', '+()*(x|a)', true],
['a.y', '+(x|a[^)]y)', true],
// Extended globs that never close stay literal
['!(a|B', '!(a|B', true],
['B', '!(a|B', false],
['?(a|B', '?(a|B', true],
['B', '?(a|B', false],
['+(a|B', '+(a|B', true],
['B', '+(a|B', false],
['*(a|B', '*(a|B', true],
['B', '*(a|B', false],
['@(a|B', '@(a|B', true],
['B', '@(a|B', false],
// Nested extglobs collapse into one set
['xy', '@(!(a|b))y', true],
['by', '@(!(a|b))y', false],
['xy', '!(a|b)y', true],
['by', '!(a|b)y', false],
['xb', 'x@(!(a|b))', false],
['xy', 'x@(!(a|b))', true],
['xb', 'x!(a|b)', false],
['x', '@(!(a|b))', true],
['b', '@(!(a|b))', false],
['a', '!(!(a|b))', true],
['x', '!(!(a|b))', false],
['ab', '?(*(a|b))', true],
['', '?(+(a|b))', true],
['ab', '@(*(a|b))', true],
['axb', '@(a*b)', true],
// Braces expand before anything else is parsed
['ab', '*(a|{b),c)}', true],
['ac', '*(a|{b),c)}', true],
['ad', '*(a|{b),c)}', false],
['ab', '*(a|{b,c})', true],
['bc', '*(a|{b|c,c})', true],
['d)', '{a,*(b|c,d)}', true],
['a{b{cdf}g}h', 'a{b{c{d,e}f}g}h', true],
['za,c}d', 'z{a,b},c}d', true],
['z{a,bcd', 'z{a,b{,c}d', true],
// `${...}` is left alone by brace expansion.
/* eslint-disable no-template-curly-in-string */
['a${c}${d}', '{a,b}${c}${d}', true],
['${a}${b}c', '${a}${b}{c,d}', true],
/* eslint-enable no-template-curly-in-string */
['/a', '{/?,*}', true],
['bb', '{/?,*}', true],
['/b/b', '{/?,*}', false],
['/asdf/asdf/asdf', '{/*,*}', false],
['c', '{c*,./c*}', true],
['a5b', 'a{00..05}b', false],
['a05b', 'a{00..05}b', true],
// Globstar and dot directories
['a/b/.x/c', '**/.x/**', true],
['a/b/.x/c/d/e', '**/.x/**', true],
['.x/a/b', '**/.x/**', true],
['a/.x/b/.x/c', '**/.x/**', false],
['.x/.y', '**/.x/**', false],
['.x/a/b', '.x/**/*', true],
['.x/a/b', '.x/*/**', true],
['.x/a/b', '.x/**/*/**', true],
['.x/.y', '.x/**/*', false],
['.x/', '.x/**/**/*', false],
['a/.d', '**', false],
['.a/.d', '**', false],
['a/b', '**', true],
['x/y/z', 'x/y/*/z', false],
['x/y/w/z', 'x/y/*/z', true],
// A segment followed by `..` cancels
['x/a/b/c', 'x/*/../a/b/c', true],
['x/y/a/b/c', 'x/*/../a/b/c', false],
['a/b/c', 'x/*/../../a/b/c', true],
['x/a/b/c', 'x/*/../../a/b/c', false],
['x/y/a/b/c', 'x/z/../*/a/b/c', true],
['x/z/a/b/c', 'x/z/../*/a/b/c', true],
['a/c/b', 'a/*/b', true],
['a/./b', 'a/*/b', false],
['a/.d/b', 'a/*/b', false],
['a/.d/b', 'a/.*/b', true],
// The optimized single-segment shapes
['a', '?', true],
['js', '??', true],
['.a', '??', false],
['.js', '???', false],
['a.js', '?.js', true],
['.js', '?js', false],
['a.js', '*.js', true],
['js', '*js', true],
['.a.js', '*.js', false],
['a.js', '*.*', true],
['.a', '*.*', false],
['.a.js', '.*', true],
['a', '*', true],
['.a', '*', false],
['x.y', '*.y', true],
['.z', '*.z', false],
];
// Most POSIX classes lower to Unicode property escapes (`\p{…}`), which V8
// only supports when built with ICU. Without it, such a pattern throws.
const kIntlClasses =
/\[:(?:alnum|alpha|blank|cntrl|digit|graph|lower|print|punct|space|upper|word):\]/;
for (const [subject, pattern, expected] of [...cases, ...minimatchCases]) {
if (!common.hasIntl && kIntlClasses.test(pattern)) continue;
assert.strictEqual(
path.posix.matchesGlob(subject, pattern),
expected,
`expected ${JSON.stringify(subject)} to ` +
`${expected ? '' : 'not '}match ${JSON.stringify(pattern)}`,
);
}
// However long a run of stars gets, it is equivalent to a single one.
assert.strictEqual(
path.posix.matchesGlob('a/b/b', `a/${'*'.repeat(100)}/b`),
path.posix.matchesGlob('a/b/b', 'a/*/b'),
);
// A pattern longer than the 64 KiB cap is rejected rather than matched.
assert.throws(() => path.matchesGlob('a', 'a'.repeat(64 * 1024 + 1)), {
name: 'TypeError',
message: 'pattern is too long',
});
assert.strictEqual(path.matchesGlob('a', 'a'.repeat(64 * 1024)), false);
// Braces and extended globs are parsed by recursing once per level of nesting,
// so a pattern that nests deeper than the parser can recurse is rejected
// instead of running the stack out. `{,` and `@(` cost two or three code units
// per level, which leaves room for thousands of levels under the 64 KiB cap.
{
const braces = (n) => `${'{,'.repeat(n)}a${'}'.repeat(n)}`;
const extglobs = (n) => `${'@('.repeat(n)}a${')'.repeat(n)}`;
assert.strictEqual(path.matchesGlob('a', braces(256)), true);
assert.strictEqual(path.matchesGlob('a', extglobs(256)), true);
for (const pattern of [
braces(257),
braces(20000),
extglobs(257),
extglobs(20000),
// An extended glob that adopts its child leaves minimatch's extDepth
// where it is, so the nesting cap is the only thing bounding this one.
'@('.repeat(20000),
]) {
assert.throws(() => path.matchesGlob('a', pattern), {
name: 'TypeError',
message: 'pattern nests too deeply',
}, pattern.slice(0, 8));
}
// fs.glob() compiles its patterns the same way.
assert.throws(() => fs.globSync(braces(20000)), {
name: 'TypeError',
message: 'pattern nests too deeply',
});
}
// Expanding braces holds literal braces aside as a code unit the pattern does
// not itself use. Naming every unit one could be drawn from leaves none free,
// which takes all 65408 units at or above U+0080 -- and that still fits under
// the 64 KiB cap. Reject such a pattern rather than rewrite one of its own
// literals into a brace.
{
let every = '';
for (let u = 0x80; u <= 0xFFFF; u++) every += String.fromCharCode(u);
assert.strictEqual(every.length, 64 * 1024 - 0x80);
assert.throws(() => path.matchesGlob('\uFDD0', `{\uFDD0,${every}}`), {
name: 'TypeError',
message: 'pattern is too long',
});
// Two spare units are all it takes to expand normally again.
assert.strictEqual(
path.matchesGlob('\uFDD0', `{\uFDD0,${every.slice(2)}}`),
true,
);
}
// Nesting past the extended glob and globstar caps stops matching rather
// than costing an unbounded amount of work.
assert.strictEqual(
path.matchesGlob('a'.repeat(100), `${'*('.repeat(100)}a${')'.repeat(100)}`),
true,
);
assert.strictEqual(path.matchesGlob('a/'.repeat(500), '**/'.repeat(500)), true);
// A nested extended glob against a long subject is what makes a backtracking
// matcher blow up: `minimatch` needs minutes on the first of these. Segments
// are matched by advancing every reachable position at once instead, so the
// cost stays bounded. A regression to backtracking would hang here.
{
const subject = `${'a'.repeat(101)}z`;
const start = process.hrtime.bigint();
for (const pattern of [
'*(*(*(a|a)))',
'*(+(*(a|b)|c)|d)',
'*(*(+(+(?(@(a|t)|u)|v)|w)|x)|y)',
'+(a|*(b|c)|d)a',
]) {
assert.strictEqual(path.posix.matchesGlob(subject, pattern), false, pattern);
}
// Chained non-adjacent globstars, from minimatch's GHSA-7r86-cg39-jmmj.
const globstars = `${Array.from({ length: 50 }, () => '**/a').join('/')}/b/**`;
assert.strictEqual(
path.posix.matchesGlob(`${Array(100).fill('a').join('/')}/a`, globstars),
false,
);
const elapsed = Number(process.hrtime.bigint() - start) / 1e6;
assert.ok(
elapsed < common.platformTimeout(5000),
`adversarial patterns took ${elapsed}ms, matching is backtracking`,
);
}
// The walker has to agree with the matcher, so run a few of the same rules
// against a real directory tree.
tmpdir.refresh();
const files = [
'a/b/c.js',
'a/b/c.ts',
'a/d.js',
'a/.hidden/e.js',
'a/.dotfile.js',
'f.js',
];
for (const file of files) {
fs.mkdirSync(tmpdir.resolve(path.dirname(file)), { recursive: true });
fs.writeFileSync(tmpdir.resolve(file), '');
}
function globbed(pattern) {
return fs.globSync(pattern, { cwd: tmpdir.path })
.map((p) => p.split(path.sep).join('/'))
.sort();
}
// `**` crosses directories but skips dot directories, `*` does not cross.
assert.deepStrictEqual(globbed('**/*.js'), ['a/b/c.js', 'a/d.js', 'f.js']);
assert.deepStrictEqual(globbed('*.js'), ['f.js']);
assert.deepStrictEqual(globbed('a/*/*.js'), ['a/b/c.js']);
// A leading dot needs a literal dot, in a file name and in a directory name.
assert.deepStrictEqual(globbed('a/.*.js'), ['a/.dotfile.js']);
assert.deepStrictEqual(globbed('a/.*/*.js'), ['a/.hidden/e.js']);
// Braces and extended globs expand the same way here as in matchesGlob().
assert.deepStrictEqual(globbed('a/b/*.{js,ts}'), ['a/b/c.js', 'a/b/c.ts']);
assert.deepStrictEqual(globbed('a/b/*.@(js|ts)'), ['a/b/c.js', 'a/b/c.ts']);
assert.deepStrictEqual(globbed('a/b/!(*.ts)'), ['a/b/c.js']);
// A `<segment>/..` pair cancels.
assert.deepStrictEqual(globbed('a/b/../d.js'), ['a/d.js']);