picomatch
2.3.12.3.2
−
Removed (1 files)
~
Modified (4 files)
Index: package/lib/constants.js
===================================================================
--- package/lib/constants.js
+++ package/lib/constants.js
@@ -3,8 +3,10 @@
const path = require('path');
const WIN_SLASH = '\\\\/';
const WIN_NO_SLASH = `[^${WIN_SLASH}]`;
+const DEFAULT_MAX_EXTGLOB_RECURSION = 0;
+
/**
* Posix glob regex
*/
@@ -66,8 +68,9 @@
* POSIX Bracket Regex
*/
const POSIX_REGEX_SOURCE = {
+ __proto__: null,
alnum: 'a-zA-Z0-9',
alpha: 'a-zA-Z',
ascii: '\\x00-\\x7F',
blank: ' \\t',
@@ -83,8 +86,9 @@
xdigit: 'A-Fa-f0-9'
};
module.exports = {
+ DEFAULT_MAX_EXTGLOB_RECURSION,
MAX_LENGTH: 1024 * 64,
POSIX_REGEX_SOURCE,
// regular expressions
@@ -96,8 +100,9 @@
REGEX_REMOVE_BACKSLASH: /(?:\[.*?[^\\]\]|\\(?=.))/g,
// Replace globs with equivalent patterns to reduce parsing time.
REPLACEMENTS: {
+ __proto__: null,
'***': '*',
'**/**': '**',
'**/**/**': '**'
}, Index: package/lib/parse.js
===================================================================
--- package/lib/parse.js
+++ package/lib/parse.js
@@ -44,8 +44,279 @@
const syntaxError = (type, char) => {
return `Missing ${type}: "${char}" - use "\\\\${char}" to match literal characters`;
};
+const splitTopLevel = input => {
+ const parts = [];
+ let bracket = 0;
+ let paren = 0;
+ let quote = 0;
+ let value = '';
+ let escaped = false;
+
+ for (const ch of input) {
+ if (escaped === true) {
+ value += ch;
+ escaped = false;
+ continue;
+ }
+
+ if (ch === '\\') {
+ value += ch;
+ escaped = true;
+ continue;
+ }
+
+ if (ch === '"') {
+ quote = quote === 1 ? 0 : 1;
+ value += ch;
+ continue;
+ }
+
+ if (quote === 0) {
+ if (ch === '[') {
+ bracket++;
+ } else if (ch === ']' && bracket > 0) {
+ bracket--;
+ } else if (bracket === 0) {
+ if (ch === '(') {
+ paren++;
+ } else if (ch === ')' && paren > 0) {
+ paren--;
+ } else if (ch === '|' && paren === 0) {
+ parts.push(value);
+ value = '';
+ continue;
+ }
+ }
+ }
+
+ value += ch;
+ }
+
+ parts.push(value);
+ return parts;
+};
+
+const isPlainBranch = branch => {
+ let escaped = false;
+
+ for (const ch of branch) {
+ if (escaped === true) {
+ escaped = false;
+ continue;
+ }
+
+ if (ch === '\\') {
+ escaped = true;
+ continue;
+ }
+
+ if (/[?*+@!()[\]{}]/.test(ch)) {
+ return false;
+ }
+ }
+
+ return true;
+};
+
+const normalizeSimpleBranch = branch => {
+ let value = branch.trim();
+ let changed = true;
+
+ while (changed === true) {
+ changed = false;
+
+ if (/^@\([^\\()[\]{}|]+\)$/.test(value)) {
+ value = value.slice(2, -1);
+ changed = true;
+ }
+ }
+
+ if (!isPlainBranch(value)) {
+ return;
+ }
+
+ return value.replace(/\\(.)/g, '$1');
+};
+
+const hasRepeatedCharPrefixOverlap = branches => {
+ const values = branches.map(normalizeSimpleBranch).filter(Boolean);
+
+ for (let i = 0; i < values.length; i++) {
+ for (let j = i + 1; j < values.length; j++) {
+ const a = values[i];
+ const b = values[j];
+ const char = a[0];
+
+ if (!char || a !== char.repeat(a.length) || b !== char.repeat(b.length)) {
+ continue;
+ }
+
+ if (a === b || a.startsWith(b) || b.startsWith(a)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+};
+
+const parseRepeatedExtglob = (pattern, requireEnd = true) => {
+ if ((pattern[0] !== '+' && pattern[0] !== '*') || pattern[1] !== '(') {
+ return;
+ }
+
+ let bracket = 0;
+ let paren = 0;
+ let quote = 0;
+ let escaped = false;
+
+ for (let i = 1; i < pattern.length; i++) {
+ const ch = pattern[i];
+
+ if (escaped === true) {
+ escaped = false;
+ continue;
+ }
+
+ if (ch === '\\') {
+ escaped = true;
+ continue;
+ }
+
+ if (ch === '"') {
+ quote = quote === 1 ? 0 : 1;
+ continue;
+ }
+
+ if (quote === 1) {
+ continue;
+ }
+
+ if (ch === '[') {
+ bracket++;
+ continue;
+ }
+
+ if (ch === ']' && bracket > 0) {
+ bracket--;
+ continue;
+ }
+
+ if (bracket > 0) {
+ continue;
+ }
+
+ if (ch === '(') {
+ paren++;
+ continue;
+ }
+
+ if (ch === ')') {
+ paren--;
+
+ if (paren === 0) {
+ if (requireEnd === true && i !== pattern.length - 1) {
+ return;
+ }
+
+ return {
+ type: pattern[0],
+ body: pattern.slice(2, i),
+ end: i
+ };
+ }
+ }
+ }
+};
+
+const getStarExtglobSequenceOutput = pattern => {
+ let index = 0;
+ const chars = [];
+
+ while (index < pattern.length) {
+ const match = parseRepeatedExtglob(pattern.slice(index), false);
+
+ if (!match || match.type !== '*') {
+ return;
+ }
+
+ const branches = splitTopLevel(match.body).map(branch => branch.trim());
+ if (branches.length !== 1) {
+ return;
+ }
+
+ const branch = normalizeSimpleBranch(branches[0]);
+ if (!branch || branch.length !== 1) {
+ return;
+ }
+
+ chars.push(branch);
+ index += match.end + 1;
+ }
+
+ if (chars.length < 1) {
+ return;
+ }
+
+ const source = chars.length === 1
+ ? utils.escapeRegex(chars[0])
+ : `[${chars.map(ch => utils.escapeRegex(ch)).join('')}]`;
+
+ return `${source}*`;
+};
+
+const repeatedExtglobRecursion = pattern => {
+ let depth = 0;
+ let value = pattern.trim();
+ let match = parseRepeatedExtglob(value);
+
+ while (match) {
+ depth++;
+ value = match.body.trim();
+ match = parseRepeatedExtglob(value);
+ }
+
+ return depth;
+};
+
+const analyzeRepeatedExtglob = (body, options) => {
+ if (options.maxExtglobRecursion === false) {
+ return { risky: false };
+ }
+
+ const max =
+ typeof options.maxExtglobRecursion === 'number'
+ ? options.maxExtglobRecursion
+ : constants.DEFAULT_MAX_EXTGLOB_RECURSION;
+
+ const branches = splitTopLevel(body).map(branch => branch.trim());
+
+ if (branches.length > 1) {
+ if (
+ branches.some(branch => branch === '') ||
+ branches.some(branch => /^[*?]+$/.test(branch)) ||
+ hasRepeatedCharPrefixOverlap(branches)
+ ) {
+ return { risky: true };
+ }
+ }
+
+ for (const branch of branches) {
+ const safeOutput = getStarExtglobSequenceOutput(branch);
+ if (safeOutput) {
+ return { risky: true, safeOutput };
+ }
+
+ if (repeatedExtglobRecursion(branch) > max) {
+ return { risky: true };
+ }
+ }
+
+ return { risky: false };
+};
+
/**
* Parse the given input string.
* @param {String} input
* @param {Object} options
@@ -225,8 +496,10 @@
token.prev = prev;
token.parens = state.parens;
token.output = state.output;
+ token.startIndex = state.index;
+ token.tokensIndex = tokens.length;
const output = (opts.capture ? '(' : '') + token.open;
increment('parens');
push({ type, value, output: state.output ? '' : ONE_CHAR });
@@ -234,8 +507,36 @@
extglobs.push(token);
};
const extglobClose = token => {
+ const literal = input.slice(token.startIndex, state.index + 1);
+ const body = input.slice(token.startIndex + 2, state.index);
+ const analysis = analyzeRepeatedExtglob(body, opts);
+
+ if ((token.type === 'plus' || token.type === 'star') && analysis.risky) {
+ const safeOutput = analysis.safeOutput
+ ? (token.output ? '' : ONE_CHAR) + (opts.capture ? `(${analysis.safeOutput})` : analysis.safeOutput)
+ : undefined;
+ const open = tokens[token.tokensIndex];
+
+ open.type = 'text';
+ open.value = literal;
+ open.output = safeOutput || utils.escapeRegex(literal);
+
+ for (let i = token.tokensIndex + 1; i < tokens.length; i++) {
+ tokens[i].value = '';
+ tokens[i].output = '';
+ delete tokens[i].suffix;
+ }
+
+ state.output = token.output + open.output;
+ state.backtrack = true;
+
+ push({ type: 'paren', extglob: true, value, output: '' });
+ decrement('parens');
+ return;
+ }
+
let output = token.close + (opts.capture ? ')' : '');
let rest;
if (token.type === 'negate') { Index: package/package.json
===================================================================
--- package/package.json
+++ package/package.json
@@ -1,8 +1,8 @@
{
"name": "picomatch",
"description": "Blazing fast and accurate glob matcher written in JavaScript, with no dependencies and full support for standard and extended Bash glob features, including braces, extglobs, POSIX brackets, and regular expressions.",
- "version": "2.3.1",
+ "version": "2.3.2",
"homepage": "https://github.com/micromatch/picomatch",
"author": "Jon Schlinkert (https://github.com/jonschlinkert)",
"funding": "https://github.com/sponsors/jonschlinkert",
"repository": "micromatch/picomatch", Index: package/README.md
===================================================================
--- package/README.md
+++ package/README.md
@@ -317,9 +317,10 @@
| `ignore` | `array\|string` | `undefined` | One or more glob patterns for excluding strings that should not be matched from the result. |
| `keepQuotes` | `boolean` | `false` | Retain quotes in the generated regex, since quotes may also be used as an alternative to backslashes. |
| `literalBrackets` | `boolean` | `undefined` | When `true`, brackets in the glob pattern will be escaped so that only literal brackets will be matched. |
| `matchBase` | `boolean` | `false` | Alias for `basename` |
-| `maxLength` | `boolean` | `65536` | Limit the max length of the input string. An error is thrown if the input string is longer than this value. |
+| `maxLength` | `number` | `65536` | Limit the max length of the input string. An error is thrown if the input string is longer than this value. |
+| `maxExtglobRecursion` | `number\|boolean` | `0` | Limit nested quantified extglobs and other risky repeated extglob forms. When the limit is exceeded, the extglob is treated as a literal string instead of being compiled to regex. Set to `false` to disable this safeguard. |
| `nobrace` | `boolean` | `false` | Disable brace matching, so that `{a,b}` and `{1..3}` would be treated as literal characters. |
| `nobracket` | `boolean` | `undefined` | Disable matching with regex brackets. |
| `nocase` | `boolean` | `false` | Make matching case-insensitive. Equivalent to the regex `i` flag. Note that this option is overridden by the `flags` option. |
| `nodupes` | `boolean` | `true` | Deprecated, use `nounique` instead. This option will be removed in a future major release. By default duplicates are removed. Disable uniquification by setting this option to false. |
@@ -532,8 +533,15 @@
console.log(pm.isMatch('foo.bar', '!(foo).!(bar)')); // false
// supports nested extglobs
console.log(pm.isMatch('foo.bar', '!(!(foo)).!(!(bar))')); // true
+
+// risky quantified extglobs are treated literally by default
+console.log(pm.makeRe('+(a|aa)'));
+//=> /^(?:\+\(a\|aa\))$/
+
+// increase the limit to allow a small amount of nested quantified extglobs
+console.log(pm.isMatch('aaa', '+(+(a))', { maxExtglobRecursion: 1 })); // true
```
#### POSIX brackets