picomatch
2.3.12.3.2
lib/parse.js~
lib/parse.jsModified+301
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') {