From ab17760505b8179721b513b3693a441a6e4cd229 Mon Sep 17 00:00:00 2001 From: chiranjib-swain Date: Wed, 25 Feb 2026 12:25:15 +0530 Subject: [PATCH] feat(minimatch): enhance globstar matching and add maxGlobstarRecursion option --- .licenses/npm/minimatch.dep.yml | 2 +- dist/index.js | 262 +++++++++++++++++++------------- package-lock.json | 20 +-- 3 files changed, 172 insertions(+), 112 deletions(-) diff --git a/.licenses/npm/minimatch.dep.yml b/.licenses/npm/minimatch.dep.yml index 05e744aa..d3fe8d9f 100644 --- a/.licenses/npm/minimatch.dep.yml +++ b/.licenses/npm/minimatch.dep.yml @@ -1,6 +1,6 @@ --- name: minimatch -version: 3.1.2 +version: 3.1.4 type: npm summary: a glob matcher in javascript homepage: diff --git a/dist/index.js b/dist/index.js index bfc793ec..43ff7d7b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -84818,6 +84818,8 @@ function Minimatch (pattern, options) { } this.options = options + this.maxGlobstarRecursion = options.maxGlobstarRecursion !== undefined + ? options.maxGlobstarRecursion : 200 this.set = [] this.pattern = pattern this.regexp = null @@ -85066,6 +85068,9 @@ function parse (pattern, isSub) { continue } + // coalesce consecutive non-globstar * characters + if (c === '*' && stateChar === '*') continue + // if we already have a stateChar, then it means // that there was something like ** or +? in there. // Handle the stateChar, then proceed with this one. @@ -85460,19 +85465,163 @@ Minimatch.prototype.match = function match (f, partial) { // out of pattern, then that's fine, as long as all // the parts match. Minimatch.prototype.matchOne = function (file, pattern, partial) { - var options = this.options + if (pattern.indexOf(GLOBSTAR) !== -1) { + return this._matchGlobstar(file, pattern, partial, 0, 0) + } + return this._matchOne(file, pattern, partial, 0, 0) +} - this.debug('matchOne', - { 'this': this, file: file, pattern: pattern }) +Minimatch.prototype._matchGlobstar = function (file, pattern, partial, fileIndex, patternIndex) { + var i - this.debug('matchOne', file.length, pattern.length) + // find first globstar from patternIndex + var firstgs = -1 + for (i = patternIndex; i < pattern.length; i++) { + if (pattern[i] === GLOBSTAR) { firstgs = i; break } + } - for (var fi = 0, - pi = 0, - fl = file.length, - pl = pattern.length - ; (fi < fl) && (pi < pl) - ; fi++, pi++) { + // find last globstar + var lastgs = -1 + for (i = pattern.length - 1; i >= 0; i--) { + if (pattern[i] === GLOBSTAR) { lastgs = i; break } + } + + var head = pattern.slice(patternIndex, firstgs) + var body = pattern.slice(firstgs + 1, lastgs) + var tail = pattern.slice(lastgs + 1) + + // check the head + if (head.length) { + var fileHead = file.slice(fileIndex, fileIndex + head.length) + if (!this._matchOne(fileHead, head, partial, 0, 0)) { + return false + } + fileIndex += head.length + } + + // check the tail + var fileTailMatch = 0 + if (tail.length) { + if (tail.length + fileIndex > file.length) return false + + var tailStart = file.length - tail.length + if (this._matchOne(file, tail, partial, tailStart, 0)) { + fileTailMatch = tail.length + } else { + // affordance for stuff like a/**/* matching a/b/ + if (file[file.length - 1] !== '' || + fileIndex + tail.length === file.length) { + return false + } + tailStart-- + if (!this._matchOne(file, tail, partial, tailStart, 0)) { + return false + } + fileTailMatch = tail.length + 1 + } + } + + // if body is empty (single ** between head and tail) + if (!body.length) { + var sawSome = !!fileTailMatch + for (i = fileIndex; i < file.length - fileTailMatch; i++) { + var f = String(file[i]) + sawSome = true + if (f === '.' || f === '..' || + (!this.options.dot && f.charAt(0) === '.')) { + return false + } + } + return sawSome + } + + // split body into segments at each GLOBSTAR + var bodySegments = [[[], 0]] + var currentBody = bodySegments[0] + var nonGsParts = 0 + var nonGsPartsSums = [0] + for (var bi = 0; bi < body.length; bi++) { + var b = body[bi] + if (b === GLOBSTAR) { + nonGsPartsSums.push(nonGsParts) + currentBody = [[], 0] + bodySegments.push(currentBody) + } else { + currentBody[0].push(b) + nonGsParts++ + } + } + + var idx = bodySegments.length - 1 + var fileLength = file.length - fileTailMatch + for (var si = 0; si < bodySegments.length; si++) { + bodySegments[si][1] = fileLength - + (nonGsPartsSums[idx--] + bodySegments[si][0].length) + } + + return !!this._matchGlobStarBodySections( + file, bodySegments, fileIndex, 0, partial, 0, !!fileTailMatch + ) +} + +// return false for "nope, not matching" +// return null for "not matching, cannot keep trying" +Minimatch.prototype._matchGlobStarBodySections = function ( + file, bodySegments, fileIndex, bodyIndex, partial, globStarDepth, sawTail +) { + var bs = bodySegments[bodyIndex] + if (!bs) { + // just make sure there are no bad dots + for (var i = fileIndex; i < file.length; i++) { + sawTail = true + var f = file[i] + if (f === '.' || f === '..' || + (!this.options.dot && f.charAt(0) === '.')) { + return false + } + } + return sawTail + } + + var body = bs[0] + var after = bs[1] + while (fileIndex <= after) { + var m = this._matchOne( + file.slice(0, fileIndex + body.length), + body, + partial, + fileIndex, + 0 + ) + // if limit exceeded, no match. intentional false negative, + // acceptable break in correctness for security. + if (m && globStarDepth < this.maxGlobstarRecursion) { + var sub = this._matchGlobStarBodySections( + file, bodySegments, + fileIndex + body.length, bodyIndex + 1, + partial, globStarDepth + 1, sawTail + ) + if (sub !== false) { + return sub + } + } + var f = file[fileIndex] + if (f === '.' || f === '..' || + (!this.options.dot && f.charAt(0) === '.')) { + return false + } + fileIndex++ + } + return null +} + +Minimatch.prototype._matchOne = function (file, pattern, partial, fileIndex, patternIndex) { + var fi, pi, fl, pl + for ( + fi = fileIndex, pi = patternIndex, fl = file.length, pl = pattern.length + ; (fi < fl) && (pi < pl) + ; fi++, pi++ + ) { this.debug('matchOne loop') var p = pattern[pi] var f = file[fi] @@ -85482,87 +85631,7 @@ Minimatch.prototype.matchOne = function (file, pattern, partial) { // should be impossible. // some invalid regexp stuff in the set. /* istanbul ignore if */ - if (p === false) return false - - if (p === GLOBSTAR) { - this.debug('GLOBSTAR', [pattern, p, f]) - - // "**" - // a/**/b/**/c would match the following: - // a/b/x/y/z/c - // a/x/y/z/b/c - // a/b/x/b/x/c - // a/b/c - // To do this, take the rest of the pattern after - // the **, and see if it would match the file remainder. - // If so, return success. - // If not, the ** "swallows" a segment, and try again. - // This is recursively awful. - // - // a/**/b/**/c matching a/b/x/y/z/c - // - a matches a - // - doublestar - // - matchOne(b/x/y/z/c, b/**/c) - // - b matches b - // - doublestar - // - matchOne(x/y/z/c, c) -> no - // - matchOne(y/z/c, c) -> no - // - matchOne(z/c, c) -> no - // - matchOne(c, c) yes, hit - var fr = fi - var pr = pi + 1 - if (pr === pl) { - this.debug('** at the end') - // a ** at the end will just swallow the rest. - // We have found a match. - // however, it will not swallow /.x, unless - // options.dot is set. - // . and .. are *never* matched by **, for explosively - // exponential reasons. - for (; fi < fl; fi++) { - if (file[fi] === '.' || file[fi] === '..' || - (!options.dot && file[fi].charAt(0) === '.')) return false - } - return true - } - - // ok, let's see if we can swallow whatever we can. - while (fr < fl) { - var swallowee = file[fr] - - this.debug('\nglobstar while', file, fr, pattern, pr, swallowee) - - // XXX remove this slice. Just pass the start index. - if (this.matchOne(file.slice(fr), pattern.slice(pr), partial)) { - this.debug('globstar found match!', fr, fl, swallowee) - // found a match. - return true - } else { - // can't swallow "." or ".." ever. - // can only swallow ".foo" when explicitly asked. - if (swallowee === '.' || swallowee === '..' || - (!options.dot && swallowee.charAt(0) === '.')) { - this.debug('dot detected!', file, fr, pattern, pr) - break - } - - // ** swallows a segment, and continue. - this.debug('globstar swallow a segment, and continue') - fr++ - } - } - - // no match was found. - // However, in partial mode, we can't say this is necessarily over. - // If there's more *pattern* left, then - /* istanbul ignore if */ - if (partial) { - // ran out of file - this.debug('\n>>> no match, partial?', file, fr, pattern, pr) - if (fr === fl) return true - } - return false - } + if (p === false || p === GLOBSTAR) return false // something other than ** // non-magic patterns just have to match exactly @@ -85579,17 +85648,6 @@ Minimatch.prototype.matchOne = function (file, pattern, partial) { if (!hit) return false } - // Note: ending in / means that we'll get a final "" - // at the end of the pattern. This can only match a - // corresponding "" at the end of the file. - // If the file ends in /, then it can only match a - // a pattern that ends in /, unless the pattern just - // doesn't have any more for it. But, a/b/ should *not* - // match "a/b/*", even though "" matches against the - // [^/]*? pattern, except in partial mode, where it might - // simply not be reached yet. - // However, a/b/ should still satisfy a/* - // now either we fell off the end of the pattern, or we're done. if (fi === fl && pi === pl) { // ran out of pattern and filename at the same time. diff --git a/package-lock.json b/package-lock.json index 3d1cba6f..77a2fda7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2280,10 +2280,11 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4020,9 +4021,9 @@ } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.8.tgz", + "integrity": "sha512-7RN35vit8DeBclkofOVmBY0eDAZZQd1HzmukRdSyz95CRh8FT54eqnbj0krQr3mrHR6sfRyYkyhwBWjoV5uqlQ==", "dev": true, "license": "ISC", "dependencies": { @@ -5944,9 +5945,10 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", + "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" },