Files
node/lib/internal/fs/glob.js
T
Archkon 111a62d9b8 fs: key glob matcher cache by platform
Glob matchers capture the path platform when they are created. Include
the platform in the cache key so path.posix and path.win32 do not share
incompatible matchers for the same pattern.

Signed-off-by: Archkon <180910180+Archkon@users.noreply.github.com>
PR-URL: https://github.com/nodejs/node/pull/64571
Fixes: https://github.com/nodejs/node/issues/64570
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Aviv Keller <me@aviv.sh>
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
2026-07-30 16:14:23 +00:00

964 lines
31 KiB
JavaScript

'use strict';
const {
ArrayFrom,
ArrayIsArray,
ArrayPrototypeAt,
ArrayPrototypeFlatMap,
ArrayPrototypeMap,
ArrayPrototypePop,
ArrayPrototypePush,
ArrayPrototypeSome,
Promise,
PromisePrototypeThen,
SafeMap,
SafeSet,
StringPrototypeEndsWith,
} = primordials;
const {
lstatSync,
readdirSync,
realpathSync,
statSync,
} = require('fs');
const {
lstat,
readdir,
realpath,
stat,
} = require('fs/promises');
const { join, resolve, basename, isAbsolute, dirname } = require('path');
const {
kEmptyObject,
isWindows,
isMacOS,
} = require('internal/util');
const {
validateBoolean,
validateObject,
validateString,
validateStringArray,
} = require('internal/validators');
const { DirentFromStats } = require('internal/fs/utils');
const {
codes: {
ERR_INVALID_ARG_TYPE,
},
hideStackFrames,
} = require('internal/errors');
const assert = require('internal/assert');
const { toPathIfFileURL } = require('internal/url');
let minimatch;
function lazyMinimatch() {
minimatch ??= require('internal/deps/minimatch/index');
return minimatch;
}
/**
* @param {string} path
* @returns {Promise<DirentFromStats|null>}
*/
async function getDirent(path) {
let stat;
try {
stat = await lstat(path);
} catch {
return null;
}
return new DirentFromStats(basename(path), stat, dirname(path));
}
/**
* @param {string} path
* @returns {DirentFromStats|null}
*/
function getDirentSync(path) {
let stat;
try {
stat = lstatSync(path);
} catch {
return null;
}
return new DirentFromStats(basename(path), stat, dirname(path));
}
/**
* @callback validateStringArrayOrFunction
* @param {*} value
* @param {string} name
*/
const validateStringArrayOrFunction = hideStackFrames((value, name) => {
if (ArrayIsArray(value)) {
for (let i = 0; i < value.length; ++i) {
if (typeof value[i] !== 'string') {
throw new ERR_INVALID_ARG_TYPE(`${name}[${i}]`, 'string', value[i]);
}
}
return;
}
if (typeof value !== 'function') {
throw new ERR_INVALID_ARG_TYPE(name, ['string[]', 'function'], value);
}
});
/**
* @param {string} pattern
* @param {options} options
* @returns {Minimatch}
*/
function createMatcher(pattern, options = kEmptyObject) {
const opts = {
__proto__: null,
nocase: isWindows || isMacOS,
windowsPathsNoEscape: true,
nonegate: true,
nocomment: true,
optimizationLevel: 2,
platform: process.platform,
nocaseMagicOnly: true,
...options,
};
return new (lazyMinimatch().Minimatch)(pattern, opts);
}
function cloneSet(values) {
const cloned = new SafeSet();
for (const value of values) {
cloned.add(value);
}
return cloned;
}
class Cache {
#cache = new SafeMap();
#statsCache = new SafeMap();
#followStatsCache = new SafeMap();
#readdirCache = new SafeMap();
#realpathCache = new SafeMap();
stat(path) {
const cached = this.#statsCache.get(path);
if (cached) {
return cached;
}
const promise = getDirent(path);
this.#statsCache.set(path, promise);
return promise;
}
statSync(path) {
const cached = this.#statsCache.get(path);
// Do not return a promise from a sync function.
if (cached && !(cached instanceof Promise)) {
return cached;
}
const val = getDirentSync(path);
this.#statsCache.set(path, val);
return val;
}
followStat(path) {
const cached = this.#followStatsCache.get(path);
if (cached) {
return cached;
}
const promise = PromisePrototypeThen(stat(path), null, () => null);
this.#followStatsCache.set(path, promise);
return promise;
}
followStatSync(path) {
const cached = this.#followStatsCache.get(path);
if (cached && !(cached instanceof Promise)) {
return cached;
}
let val;
try {
val = statSync(path);
} catch {
val = null;
}
this.#followStatsCache.set(path, val);
return val;
}
realpath(path) {
const cached = this.#realpathCache.get(path);
if (cached) {
return cached;
}
const promise = PromisePrototypeThen(realpath(path), null, () => null);
this.#realpathCache.set(path, promise);
return promise;
}
realpathSync(path) {
const cached = this.#realpathCache.get(path);
if (cached && !(cached instanceof Promise)) {
return cached;
}
let val;
try {
val = realpathSync(path);
} catch {
val = null;
}
this.#realpathCache.set(path, val);
return val;
}
addToStatCache(path, val) {
this.#statsCache.set(path, val);
}
async readdir(path) {
const cached = this.#readdirCache.get(path);
if (cached) {
return cached;
}
const promise = PromisePrototypeThen(readdir(path, { __proto__: null, withFileTypes: true }), null, () => []);
this.#readdirCache.set(path, promise);
return promise;
}
readdirSync(path) {
const cached = this.#readdirCache.get(path);
if (cached) {
return cached;
}
let val;
try {
val = readdirSync(path, { __proto__: null, withFileTypes: true });
} catch {
val = [];
}
this.#readdirCache.set(path, val);
return val;
}
add(path, pattern) {
let cache = this.#cache.get(path);
if (!cache) {
cache = new SafeSet();
this.#cache.set(path, cache);
}
const originalSize = cache.size;
pattern.indexes.forEach((index) => cache.add(pattern.cacheKey(index)));
return cache.size !== originalSize + pattern.indexes.size;
}
seen(path, pattern, index) {
return this.#cache.get(path)?.has(pattern.cacheKey(index));
}
}
class Pattern {
#pattern;
#globStrings;
indexes;
symlinks;
realpaths;
last;
constructor(pattern, globStrings, indexes, symlinks, realpaths = new SafeSet()) {
this.#pattern = pattern;
this.#globStrings = globStrings;
this.indexes = indexes;
this.symlinks = symlinks;
this.realpaths = realpaths;
this.last = pattern.length - 1;
}
isLast(isDirectory) {
return this.indexes.has(this.last) ||
(this.at(-1) === '' && isDirectory &&
this.indexes.has(this.last - 1) && this.at(-2) === lazyMinimatch().GLOBSTAR);
}
isFirst() {
return this.indexes.has(0);
}
get hasSeenSymlinks() {
return ArrayPrototypeSome(ArrayFrom(this.indexes), (i) => !this.symlinks.has(i));
}
at(index) {
return ArrayPrototypeAt(this.#pattern, index);
}
child(indexes, symlinks = new SafeSet(), realpaths = this.realpaths) {
return new Pattern(this.#pattern, this.#globStrings, indexes, symlinks, realpaths);
}
test(index, path) {
if (index > this.#pattern.length) {
return false;
}
const pattern = this.#pattern[index];
if (pattern === lazyMinimatch().GLOBSTAR) {
return true;
}
if (typeof pattern === 'string') {
return pattern === path;
}
if (typeof pattern?.test === 'function') {
return pattern.test(path);
}
return false;
}
cacheKey(index) {
let key = '';
for (let i = index; i < this.#globStrings.length; i++) {
key += this.#globStrings[i];
if (i !== this.#globStrings.length - 1) {
key += '/';
}
}
return key;
}
}
class ResultSet extends SafeSet {
#root = '.';
#isExcluded = () => false;
setup(root, isExcludedFn) {
this.#root = root;
this.#isExcluded = isExcludedFn;
}
add(value) {
if (this.#isExcluded(resolve(this.#root, value))) {
return false;
}
super.add(value);
return true;
}
}
class Glob {
#root;
#exclude;
#cache = new Cache();
#results = new ResultSet();
#queue = [];
#subpatterns = new SafeMap();
#patterns;
#withFileTypes;
#followSymlinks = false;
#isExcluded = () => false;
constructor(pattern, options = kEmptyObject) {
validateObject(options, 'options');
const { exclude, cwd, followSymlinks, withFileTypes } = options;
this.#root = toPathIfFileURL(cwd) ?? '.';
if (followSymlinks != null) {
validateBoolean(followSymlinks, 'options.followSymlinks');
this.#followSymlinks = followSymlinks;
}
this.#withFileTypes = !!withFileTypes;
if (exclude != null) {
validateStringArrayOrFunction(exclude, 'options.exclude');
if (ArrayIsArray(exclude)) {
assert(typeof this.#root === 'string');
// Convert the path part of exclude patterns to absolute paths for
// consistent comparison before instantiating matchers.
const matchers = exclude
.map((pattern) => resolve(this.#root, pattern))
.map((pattern) => createMatcher(pattern));
this.#isExcluded = (value) =>
matchers.some((matcher) => matcher.match(value));
this.#results.setup(this.#root, this.#isExcluded);
} else {
this.#exclude = exclude;
}
}
let patterns;
if (typeof pattern === 'object') {
validateStringArray(pattern, 'patterns');
patterns = pattern;
} else {
validateString(pattern, 'patterns');
patterns = [pattern];
}
this.matchers = ArrayPrototypeMap(patterns, (pattern) => createMatcher(pattern));
this.#patterns = ArrayPrototypeFlatMap(this.matchers, (matcher) => ArrayPrototypeMap(matcher.set,
(pattern, i) => new Pattern(
pattern,
matcher.globParts[i],
new SafeSet().add(0),
new SafeSet(),
)));
}
globSync() {
ArrayPrototypePush(this.#queue, { __proto__: null, path: '.', patterns: this.#patterns });
while (this.#queue.length > 0) {
const item = ArrayPrototypePop(this.#queue);
for (let i = 0; i < item.patterns.length; i++) {
this.#addSubpatterns(item.path, item.patterns[i]);
}
this.#subpatterns
.forEach((patterns, path) => ArrayPrototypePush(this.#queue, { __proto__: null, path, patterns }));
this.#subpatterns.clear();
}
return ArrayFrom(
this.#results,
this.#withFileTypes ? (path) => this.#cache.statSync(
isAbsolute(path) ?
path :
join(this.#root, path),
) : undefined,
);
}
#isDirectorySync(path, stat, pattern) {
if (stat?.isDirectory()) {
return true;
}
if (!stat?.isSymbolicLink()) {
return false;
}
if (this.#followSymlinks) {
return !!this.#cache.followStatSync(path)?.isDirectory();
}
return pattern.hasSeenSymlinks;
}
async #isDirectory(path, stat, pattern) {
if (stat?.isDirectory()) {
return true;
}
if (!stat?.isSymbolicLink()) {
return false;
}
if (this.#followSymlinks) {
return !!(await this.#cache.followStat(path))?.isDirectory();
}
return pattern.hasSeenSymlinks;
}
#nextRealpathsSync(path, isDirectory, pattern) {
if (!this.#followSymlinks || !isDirectory) {
return pattern.realpaths;
}
const real = this.#cache.realpathSync(path);
if (real === null) {
return pattern.realpaths;
}
const realpaths = cloneSet(pattern.realpaths);
realpaths.add(real);
return realpaths;
}
async #nextRealpaths(path, isDirectory, pattern) {
if (!this.#followSymlinks || !isDirectory) {
return pattern.realpaths;
}
const real = await this.#cache.realpath(path);
if (real === null) {
return pattern.realpaths;
}
const realpaths = cloneSet(pattern.realpaths);
realpaths.add(real);
return realpaths;
}
async #isCyclic(path, isDirectory, pattern) {
if (!this.#followSymlinks || !isDirectory) {
return false;
}
const real = await this.#cache.realpath(path);
return real !== null && pattern.realpaths.has(real);
}
#isCyclicSync(path, isDirectory, pattern) {
if (!this.#followSymlinks || !isDirectory) {
return false;
}
const real = this.#cache.realpathSync(path);
return real !== null && pattern.realpaths.has(real);
}
#addSubpattern(path, pattern) {
if (this.#isExcluded(path)) {
return;
}
const fullpath = resolve(this.#root, path);
// If path is a directory, add trailing slash and test patterns again.
if (this.#isExcluded(`${fullpath}/`) && this.#cache.statSync(fullpath).isDirectory()) {
return;
}
if (this.#exclude) {
if (this.#withFileTypes) {
const stat = this.#cache.statSync(path);
if (stat !== null) {
if (this.#exclude(stat)) {
return;
}
}
} else if (this.#exclude(path)) {
return;
}
}
if (!this.#subpatterns.has(path)) {
this.#subpatterns.set(path, [pattern]);
} else {
ArrayPrototypePush(this.#subpatterns.get(path), pattern);
}
}
#addSubpatterns(path, pattern) {
const seen = this.#cache.add(path, pattern);
if (seen) {
return;
}
const fullpath = resolve(this.#root, path);
const stat = this.#cache.statSync(fullpath);
const last = pattern.last;
const isDirectory = this.#isDirectorySync(fullpath, stat, pattern);
const isLast = pattern.isLast(isDirectory);
const isFirst = pattern.isFirst();
if (this.#isExcluded(fullpath)) {
return;
}
if (isFirst && isWindows && typeof pattern.at(0) === 'string' && StringPrototypeEndsWith(pattern.at(0), ':')) {
// Absolute path, go to root
this.#addSubpattern(`${pattern.at(0)}\\`, pattern.child(new SafeSet().add(1)));
return;
}
if (isFirst && pattern.at(0) === '') {
// Absolute path, go to root
this.#addSubpattern('/', pattern.child(new SafeSet().add(1)));
return;
}
if (isFirst && pattern.at(0) === '..') {
// Start with .., go to parent
this.#addSubpattern('../', pattern.child(new SafeSet().add(1)));
return;
}
if (isFirst && pattern.at(0) === '.') {
// Start with ., proceed
this.#addSubpattern('.', pattern.child(new SafeSet().add(1)));
return;
}
if (isLast && typeof pattern.at(-1) === 'string') {
// Add result if it exists
const p = pattern.at(-1);
const stat = this.#cache.statSync(join(fullpath, p));
if (stat && (p || isDirectory)) {
this.#results.add(join(path, p));
}
if (pattern.indexes.size === 1 && pattern.indexes.has(last)) {
return;
}
} else if (isLast && pattern.at(-1) === lazyMinimatch().GLOBSTAR &&
(path !== '.' || pattern.at(0) === '.' || (last === 0 && stat))) {
// If pattern ends with **, add to results
// if path is ".", add it only if pattern starts with "." or pattern is exactly "**"
this.#results.add(path);
}
if (!isDirectory || this.#isCyclicSync(fullpath, isDirectory, pattern)) {
return;
}
const nextRealpaths = this.#nextRealpathsSync(fullpath, isDirectory, pattern);
let children;
const firstPattern = pattern.indexes.size === 1 && pattern.at(pattern.indexes.values().next().value);
if (typeof firstPattern === 'string') {
const stat = this.#cache.statSync(join(fullpath, firstPattern));
if (stat) {
stat.name = firstPattern;
children = [stat];
} else {
return;
}
} else {
children = this.#cache.readdirSync(fullpath);
}
for (let i = 0; i < children.length; i++) {
const entry = children[i];
const entryPath = join(path, entry.name);
const entryFullpath = join(fullpath, entry.name);
this.#cache.addToStatCache(entryFullpath, entry);
const entryIsDirectory = entry.isDirectory() ||
(this.#followSymlinks && entry.isSymbolicLink() && !!this.#cache.followStatSync(entryFullpath)?.isDirectory());
const subPatterns = new SafeSet();
const nSymlinks = new SafeSet();
for (const index of pattern.indexes) {
// For each child, check potential patterns
if (this.#cache.seen(entryPath, pattern, index) || this.#cache.seen(entryPath, pattern, index + 1)) {
return;
}
const current = pattern.at(index);
const nextIndex = index + 1;
const next = pattern.at(nextIndex);
const fromSymlink = !this.#followSymlinks && pattern.symlinks.has(index);
if (current === lazyMinimatch().GLOBSTAR) {
const isDot = entry.name[0] === '.';
const nextMatches = pattern.test(nextIndex, entry.name);
let nextNonGlobIndex = nextIndex;
while (pattern.at(nextNonGlobIndex) === lazyMinimatch().GLOBSTAR) {
nextNonGlobIndex++;
}
const matchesDot = isDot && pattern.test(nextNonGlobIndex, entry.name);
if ((isDot && !matchesDot) ||
(this.#exclude && this.#exclude(this.#withFileTypes ? entry : entry.name))) {
continue;
}
if (!fromSymlink && entryIsDirectory) {
// If directory, add ** to its potential patterns
subPatterns.add(index);
} else if (!fromSymlink && index === last) {
// If ** is last, add to results
this.#results.add(entryPath);
}
// Any pattern after ** is also a potential pattern
// so we can already test it here
if (nextMatches && nextIndex === last && !isLast) {
// If next pattern is the last one, add to results
this.#results.add(entryPath);
} else if (nextMatches && entryIsDirectory) {
// Pattern matched, meaning two patterns forward
// are also potential patterns
// e.g **/b/c when entry is a/b - add c to potential patterns
subPatterns.add(index + 2);
}
if ((nextMatches || pattern.at(0) === '.') &&
(entryIsDirectory || entry.isSymbolicLink()) && !fromSymlink) {
// If pattern after ** matches, or pattern starts with "."
// and entry is a directory or symlink, add to potential patterns
subPatterns.add(nextIndex);
}
if (!this.#followSymlinks && entry.isSymbolicLink()) {
nSymlinks.add(index);
}
if (next === '..' && entryIsDirectory) {
// In case pattern is "**/..",
// both parent and current directory should be added to the queue
// if this is the last pattern, add to results instead
const parent = join(path, '..');
if (nextIndex < last) {
if (!this.#subpatterns.has(path) && !this.#cache.seen(path, pattern, nextIndex + 1)) {
this.#subpatterns.set(path, [pattern.child(new SafeSet().add(nextIndex + 1))]);
}
if (!this.#subpatterns.has(parent) && !this.#cache.seen(parent, pattern, nextIndex + 1)) {
this.#subpatterns.set(parent, [pattern.child(new SafeSet().add(nextIndex + 1))]);
}
} else {
if (!this.#cache.seen(path, pattern, nextIndex)) {
this.#cache.add(path, pattern.child(new SafeSet().add(nextIndex)));
this.#results.add(path);
}
if (!this.#cache.seen(path, pattern, nextIndex) || !this.#cache.seen(parent, pattern, nextIndex)) {
this.#cache.add(parent, pattern.child(new SafeSet().add(nextIndex)));
this.#results.add(parent);
}
}
}
}
if (typeof current === 'string') {
if (pattern.test(index, entry.name) && index !== last) {
// If current pattern matches entry name
// the next pattern is a potential pattern
subPatterns.add(nextIndex);
} else if (current === '.' && pattern.test(nextIndex, entry.name)) {
// If current pattern is ".", proceed to test next pattern
if (nextIndex === last) {
this.#results.add(entryPath);
} else {
subPatterns.add(nextIndex + 1);
}
}
}
if (typeof current === 'object' && pattern.test(index, entry.name)) {
// If current pattern is a regex that matches entry name (e.g *.js)
// add next pattern to potential patterns, or to results if it's the last pattern
if (index === last) {
this.#results.add(entryPath);
} else if (entryIsDirectory) {
subPatterns.add(nextIndex);
}
}
}
if (subPatterns.size > 0) {
// If there are potential patterns, add to queue
this.#addSubpattern(entryPath, pattern.child(subPatterns, nSymlinks, nextRealpaths));
}
}
}
async* glob() {
ArrayPrototypePush(this.#queue, { __proto__: null, path: '.', patterns: this.#patterns });
while (this.#queue.length > 0) {
const item = ArrayPrototypePop(this.#queue);
for (let i = 0; i < item.patterns.length; i++) {
yield* this.#iterateSubpatterns(item.path, item.patterns[i]);
}
this.#subpatterns
.forEach((patterns, path) => ArrayPrototypePush(this.#queue, { __proto__: null, path, patterns }));
this.#subpatterns.clear();
}
}
async* #iterateSubpatterns(path, pattern) {
const seen = this.#cache.add(path, pattern);
if (seen) {
return;
}
const fullpath = resolve(this.#root, path);
const stat = await this.#cache.stat(fullpath);
const last = pattern.last;
const isDirectory = await this.#isDirectory(fullpath, stat, pattern);
const isLast = pattern.isLast(isDirectory);
const isFirst = pattern.isFirst();
if (this.#isExcluded(fullpath)) {
return;
}
if (isFirst && isWindows && typeof pattern.at(0) === 'string' && StringPrototypeEndsWith(pattern.at(0), ':')) {
// Absolute path, go to root
this.#addSubpattern(`${pattern.at(0)}\\`, pattern.child(new SafeSet().add(1)));
return;
}
if (isFirst && pattern.at(0) === '') {
// Absolute path, go to root
this.#addSubpattern('/', pattern.child(new SafeSet().add(1)));
return;
}
if (isFirst && pattern.at(0) === '..') {
// Start with .., go to parent
this.#addSubpattern('../', pattern.child(new SafeSet().add(1)));
return;
}
if (isFirst && pattern.at(0) === '.') {
// Start with ., proceed
this.#addSubpattern('.', pattern.child(new SafeSet().add(1)));
return;
}
if (isLast && typeof pattern.at(-1) === 'string') {
// Add result if it exists
const p = pattern.at(-1);
const stat = await this.#cache.stat(join(fullpath, p));
if (stat && (p || isDirectory)) {
const result = join(path, p);
if (!this.#results.has(result)) {
if (this.#results.add(result)) {
yield this.#withFileTypes ? stat : result;
}
}
}
if (pattern.indexes.size === 1 && pattern.indexes.has(last)) {
return;
}
} else if (isLast && pattern.at(-1) === lazyMinimatch().GLOBSTAR &&
(path !== '.' || pattern.at(0) === '.' || (last === 0 && stat))) {
// If pattern ends with **, add to results
// if path is ".", add it only if pattern starts with "." or pattern is exactly "**"
if (!this.#results.has(path)) {
if (this.#results.add(path)) {
yield this.#withFileTypes ? stat : path;
}
}
}
if (!isDirectory || await this.#isCyclic(fullpath, isDirectory, pattern)) {
return;
}
const nextRealpaths = await this.#nextRealpaths(fullpath, isDirectory, pattern);
let children;
const firstPattern = pattern.indexes.size === 1 && pattern.at(pattern.indexes.values().next().value);
if (typeof firstPattern === 'string') {
const stat = await this.#cache.stat(join(fullpath, firstPattern));
if (stat) {
stat.name = firstPattern;
children = [stat];
} else {
return;
}
} else {
children = await this.#cache.readdir(fullpath);
}
for (let i = 0; i < children.length; i++) {
const entry = children[i];
const entryPath = join(path, entry.name);
const entryFullpath = join(fullpath, entry.name);
this.#cache.addToStatCache(entryFullpath, entry);
const entryIsDirectory = entry.isDirectory() ||
(this.#followSymlinks &&
entry.isSymbolicLink() &&
!!(await this.#cache.followStat(entryFullpath))?.isDirectory());
const subPatterns = new SafeSet();
const nSymlinks = new SafeSet();
for (const index of pattern.indexes) {
// For each child, check potential patterns
if (this.#cache.seen(entryPath, pattern, index) || this.#cache.seen(entryPath, pattern, index + 1)) {
return;
}
const current = pattern.at(index);
const nextIndex = index + 1;
const next = pattern.at(nextIndex);
const fromSymlink = !this.#followSymlinks && pattern.symlinks.has(index);
if (current === lazyMinimatch().GLOBSTAR) {
const isDot = entry.name[0] === '.';
const nextMatches = pattern.test(nextIndex, entry.name);
let nextNonGlobIndex = nextIndex;
while (pattern.at(nextNonGlobIndex) === lazyMinimatch().GLOBSTAR) {
nextNonGlobIndex++;
}
const matchesDot = isDot && pattern.test(nextNonGlobIndex, entry.name);
if ((isDot && !matchesDot) ||
(this.#exclude && this.#exclude(this.#withFileTypes ? entry : entry.name))) {
continue;
}
if (!fromSymlink && entryIsDirectory) {
// If directory, add ** to its potential patterns
subPatterns.add(index);
} else if (!fromSymlink && index === last) {
// If ** is last, add to results
if (!this.#results.has(entryPath) && this.#results.add(entryPath)) {
yield this.#withFileTypes ? entry : entryPath;
}
}
// Any pattern after ** is also a potential pattern
// so we can already test it here
if (nextMatches && nextIndex === last && !isLast) {
// If next pattern is the last one, add to results
if (!this.#results.has(entryPath) && this.#results.add(entryPath)) {
yield this.#withFileTypes ? entry : entryPath;
}
} else if (nextMatches && entryIsDirectory) {
// Pattern matched, meaning two patterns forward
// are also potential patterns
// e.g **/b/c when entry is a/b - add c to potential patterns
subPatterns.add(index + 2);
}
if ((nextMatches || pattern.at(0) === '.') &&
(entryIsDirectory || entry.isSymbolicLink()) && !fromSymlink) {
// If pattern after ** matches, or pattern starts with "."
// and entry is a directory or symlink, add to potential patterns
subPatterns.add(nextIndex);
}
if (!this.#followSymlinks && entry.isSymbolicLink()) {
nSymlinks.add(index);
}
if (next === '..' && entryIsDirectory) {
// In case pattern is "**/..",
// both parent and current directory should be added to the queue
// if this is the last pattern, add to results instead
const parent = join(path, '..');
if (nextIndex < last) {
if (!this.#subpatterns.has(path) && !this.#cache.seen(path, pattern, nextIndex + 1)) {
this.#subpatterns.set(path, [pattern.child(new SafeSet().add(nextIndex + 1))]);
}
if (!this.#subpatterns.has(parent) && !this.#cache.seen(parent, pattern, nextIndex + 1)) {
this.#subpatterns.set(parent, [pattern.child(new SafeSet().add(nextIndex + 1))]);
}
} else {
if (!this.#cache.seen(path, pattern, nextIndex)) {
this.#cache.add(path, pattern.child(new SafeSet().add(nextIndex)));
if (!this.#results.has(path)) {
if (this.#results.add(path)) {
yield this.#withFileTypes ? this.#cache.statSync(fullpath) : path;
}
}
}
if (!this.#cache.seen(path, pattern, nextIndex) || !this.#cache.seen(parent, pattern, nextIndex)) {
this.#cache.add(parent, pattern.child(new SafeSet().add(nextIndex)));
if (!this.#results.has(parent)) {
if (this.#results.add(parent)) {
yield this.#withFileTypes ? this.#cache.statSync(join(this.#root, parent)) : parent;
}
}
}
}
}
}
if (typeof current === 'string') {
if (pattern.test(index, entry.name) && index !== last) {
// If current pattern matches entry name
// the next pattern is a potential pattern
subPatterns.add(nextIndex);
} else if (current === '.' && pattern.test(nextIndex, entry.name)) {
// If current pattern is ".", proceed to test next pattern
if (nextIndex === last) {
if (!this.#results.has(entryPath)) {
if (this.#results.add(entryPath)) {
yield this.#withFileTypes ? entry : entryPath;
}
}
} else {
subPatterns.add(nextIndex + 1);
}
}
}
if (typeof current === 'object' && pattern.test(index, entry.name)) {
// If current pattern is a regex that matches entry name (e.g *.js)
// add next pattern to potential patterns, or to results if it's the last pattern
if (index === last) {
if (!this.#results.has(entryPath)) {
if (this.#results.add(entryPath)) {
yield this.#withFileTypes ? entry : entryPath;
}
}
} else if (entryIsDirectory) {
subPatterns.add(nextIndex);
}
}
}
if (subPatterns.size > 0) {
// If there are potential patterns, add to queue
this.#addSubpattern(entryPath, pattern.child(subPatterns, nSymlinks, nextRealpaths));
}
}
}
}
const kGlobPatternCacheLimit = 250;
const patternFnCache = new SafeMap();
/**
* Check if a path matches a glob pattern
* @param {string} path the path to check
* @param {string} pattern the glob pattern to match
* @param {boolean} windows whether the path is on a Windows system, defaults to `isWindows`
* @returns {boolean}
*/
function matchGlobPattern(path, pattern, windows = isWindows) {
validateString(path, 'path');
validateString(pattern, 'pattern');
const cacheKey = `${windows ? 'win32' : 'posix'}:${pattern}`;
let matcher;
if (patternFnCache.has(cacheKey)) {
matcher = patternFnCache.get(cacheKey);
} else {
matcher = createMatcher(pattern, {
kEmptyObject,
platform: windows ? 'win32' : 'posix',
});
patternFnCache.set(cacheKey, matcher);
if (patternFnCache.size >= kGlobPatternCacheLimit) {
patternFnCache.delete(patternFnCache.keys().next().value);
}
}
return matcher.match(path);
}
module.exports = {
__proto__: null,
Glob,
createMatcher,
matchGlobPattern,
};