From 2e565263461b2136e4e960185236b1f83c93e407 Mon Sep 17 00:00:00 2001 From: Jon Schlinkert Date: Sun, 23 Feb 2020 00:49:39 -0500 Subject: [PATCH] fixes https://github.com/micromatch/picomatch/issues/62 as well as a few other minor optimizations and linting. --- .eslintrc.json | 14 +++---- .verb.md | 1 + bench/glob-parent.js | 10 ++--- bench/index.js | 33 ++++++++------- bench/load-time.js | 13 +++++- examples/scan.js | 59 ++++++++++++++++++++++++++ lib/parse.js | 37 ++++++++++++----- lib/picomatch.js | 48 +++++++++++++-------- lib/scan.js | 85 ++++++++++++++++++++++---------------- test/api.picomatch.js | 6 +-- test/api.scan.js | 14 ++++++- test/dotfiles.js | 4 +- test/extglobs.js | 2 +- test/malicious.js | 4 +- test/parens.js | 12 ++++++ test/special-characters.js | 2 +- test/stars.js | 2 + 17 files changed, 244 insertions(+), 102 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 72926e7..4c1d3fb 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,19 +1,18 @@ { - "extends": [ - "eslint:recommended" - ], + "extends": ["eslint:recommended"], "env": { "es6": true, "node": true }, - "parserOptions":{ + "parserOptions": { "ecmaVersion": 9 }, "rules": { "accessor-pairs": 2, + "arrow-parens": [2, "as-needed"], "arrow-spacing": [2, { "before": true, "after": true }], "block-spacing": [2, "always"], "brace-style": [2, "1tbs", { "allowSingleLine": true }], @@ -26,7 +25,7 @@ "eol-last": 2, "eqeqeq": [2, "allow-null"], "generator-star-spacing": [2, { "before": true, "after": true }], - "handle-callback-err": [2, "^(err|error)$" ], + "handle-callback-err": [2, "^(err|error)$"], "indent": [2, 2, { "SwitchCase": 1 }], "key-spacing": [2, { "beforeColon": false, "afterColon": true }], "keyword-spacing": [2, { "before": true, "after": true }], @@ -36,7 +35,6 @@ "no-caller": 2, "no-class-assign": 2, "no-cond-assign": 2, - "no-console": 0, "no-const-assign": 2, "no-control-regex": 2, "no-debugger": 2, @@ -104,13 +102,13 @@ "one-var": [2, { "initialized": "never" }], "operator-linebreak": [0, "after", { "overrides": { "?": "before", ":": "before" } }], "padded-blocks": [0, "never"], - "prefer-const": 2, + "prefer-const": [2, { "destructuring": "all", "ignoreReadBeforeAssign": false }], "quotes": [2, "single", "avoid-escape"], "radix": 2, "semi": [2, "always"], "semi-spacing": [2, { "before": false, "after": true }], "space-before-blocks": [2, "always"], - "space-before-function-paren": [2, "never"], + "space-before-function-paren": [2, { "anonymous": "never", "named": "never", "asyncArrow": "always" }], "space-in-parens": [2, "never"], "space-infix-ops": 2, "space-unary-ops": [2, { "words": true, "nonwords": false }], diff --git a/.verb.md b/.verb.md index a8cabdd..8fbb4b7 100644 --- a/.verb.md +++ b/.verb.md @@ -125,6 +125,7 @@ The following options may be used with the main `picomatch()` function or any of | `posixSlashes` | `boolean` | `undefined` | Convert all slashes in file paths to forward slashes. This does not convert slashes in the glob pattern itself | | `prepend` | `boolean` | `undefined` | String to prepend to the generated regex used for matching. | | `regex` | `boolean` | `false` | Use regular expression rules for `+` (instead of matching literal `+`), and for stars that follow closing parentheses or brackets (as in `)*` and `]*`). | +| `strictBraces` | `boolean` | `undefined` | Treat brace patterns with no sets or ranges as literals. For example, `{abc}` would be a literal. | | `strictBrackets` | `boolean` | `undefined` | Throw an error if brackets, braces, or parens are imbalanced. | | `strictSlashes` | `boolean` | `undefined` | When true, picomatch won't match trailing slashes with single stars. | | `unescape` | `boolean` | `undefined` | Remove backslashes preceding escaped characters in the glob pattern. By default, backslashes are retained. | diff --git a/bench/glob-parent.js b/bench/glob-parent.js index 2790415..9b3e770 100644 --- a/bench/glob-parent.js +++ b/bench/glob-parent.js @@ -47,11 +47,6 @@ bench('*.js') .add(' glob-parent', () => parent('*.js')) .run(); -bench('foo/bar/baz') - .add('picomatch.scan', () => scan('foo/bar/baz')) - .add(' glob-parent', () => parent('foo/bar/baz')) - .run(); - bench('foo/*.js') .add('picomatch.scan', () => scan('foo/*.js')) .add(' glob-parent', () => parent('foo/*.js')) @@ -62,6 +57,11 @@ bench('foo/{a,b}/*.js') .add(' glob-parent', () => parent('foo/{a,b}/*.js')) .run(); +bench('foo/bar/baz') + .add('picomatch.scan', () => scan('foo/bar/baz')) + .add(' glob-parent', () => parent('foo/bar/baz')) + .run(); + bench('*.js { parts: true, tokens: true }') .add('picomatch.scan', () => scan('*.js', { parts: true, tokens: true })) .add(' glob-parent', () => parent('*.js')) diff --git a/bench/index.js b/bench/index.js index cf12e43..55778ba 100644 --- a/bench/index.js +++ b/bench/index.js @@ -44,31 +44,36 @@ const bench = (name, options) => { }; bench(`${red('.makeRe')} star`) - .add('picomatch', () => pm.makeRe('*')) + .add('picomatch', () => pm.makeRe('*', { fastpaths: false })) .add('minimatch', () => mm.makeRe('*')) .run(); -bench(`${red('.makeRe')} star; dot=true`) - .add('picomatch', () => pm.makeRe('*', { dot: true })) +bench(`${red('.makeRe')} leading star`) + .add('picomatch', () => pm.makeRe('*.txt', { fastpaths: false })) + .add('minimatch', () => mm.makeRe('*.txt')) + .run(); + +bench(`${red('.makeRe')} path with star`) + .add('picomatch', () => pm.makeRe('foo/*.js', { fastpaths: false })) + .add('minimatch', () => mm.makeRe('foo/*.js')) + .run(); + +bench(`${red('.makeRe')} star w/ { dot: true }`) + .add('picomatch', () => pm.makeRe('*', { dot: true , fastpaths: false })) .add('minimatch', () => mm.makeRe('*', { dot: true })) .run(); bench(`${red('.makeRe')} globstar`) - .add('picomatch', () => pm.makeRe('**')) + .add('picomatch', () => pm.makeRe('**', { fastpaths: false })) .add('minimatch', () => mm.makeRe('**')) .run(); -bench(`${red('.makeRe')} globstars`) - .add('picomatch', () => pm.makeRe('**/**/**')) +bench(`${red('.makeRe')} multiple globstars`) + .add('picomatch', () => pm.makeRe('**/**/**', { fastpaths: false })) .add('minimatch', () => mm.makeRe('**/**/**')) .run(); -bench(`${red('.makeRe')} with leading star`) - .add('picomatch', () => pm.makeRe('*.txt')) - .add('minimatch', () => mm.makeRe('*.txt')) - .run(); - -bench(`${red('.makeRe')} - basic braces`) - .add('picomatch', () => pm.makeRe('{a,b,c}*.txt')) - .add('minimatch', () => mm.makeRe('{a,b,c}*.txt')) +bench(`${red('.makeRe')} basic braces`) + .add('picomatch', () => pm.makeRe('foo/{a,b,c}*.txt', { fastpaths: false })) + .add('minimatch', () => mm.makeRe('foo/{a,b,c}*.txt')) .run(); diff --git a/bench/load-time.js b/bench/load-time.js index b5e5c3a..b66eda4 100644 --- a/bench/load-time.js +++ b/bench/load-time.js @@ -1,10 +1,19 @@ 'use strict'; +const libs = { + get pm() { + return require('..'); + }, + get mm() { + return require('minimatch'); + } +}; + console.log('# Load time'); console.time('picomatch'); -exports.pm = require('..'); +libs.pm; console.timeEnd('picomatch'); console.time('minimatch'); -exports.mm = require('minimatch'); +libs.mm; console.timeEnd('minimatch'); console.log(); diff --git a/examples/scan.js b/examples/scan.js index 962422d..98e53ea 100644 --- a/examples/scan.js +++ b/examples/scan.js @@ -15,3 +15,62 @@ console.log(pm.scan('foo/**/*.js')); console.log(pm.scan('foo/bar/*.js')); console.log(pm.scan('foo/*.js')); console.log(pm.scan('/foo')); + + +const braces = require('braces'); + +const scan = (pattern, options) => { + const cache = new Map(); + const matchers = {}; + const patterns = braces.expand(pattern, options); + const result = patterns.map(p => pm.scan(p, options)); + + for (let i = 0; i < result.length; i++) { + const state = result[i]; + if (state.maxDepth === Infinity) continue; + const matcher = matchers[state.base] || (matchers[state.base] = {}); + let foundGlob = false; + + for (const token of state.tokens) { + if (token.isGlob === true) { + foundGlob = true; + } + + if (foundGlob === false) { + continue; + } + + if (token.isGlob === false) { + token.matcher = name => token.value === name; + } else { + token.matcher = function glob() {} + } + + + + } + console.log(state); + + } + + return result; +}; + +scan('{one/two,foo}/*/abc/{bar,**/*}.js', { parts: true, tokens: true }); + +// scan('./foo/**/*/*.js', { parts: true, tokens: true }); +// scan('**/bar.js', { parts: true, tokens: true }); +// scan('foo/**/bar.js', { parts: true, tokens: true }); +// scan('foo/**/{bar,*/*}.js', { parts: true, tokens: true }); +// scan('foo/**/{bar,*/*}/*.js', { parts: true, tokens: true }); +// const { tokens } = scan('foo/*/{bar,*/*}/*.js', { parts: true, tokens: true }); +// for (const token of tokens) { +// console.log(token); +// } + +// console.log(scan('./foo/**/*/*.js', { parts: true, tokens: true })); +// console.log(scan('foo/**/bar.js', { parts: true, tokens: true })); +// console.log(scan('foo/**/{bar,*/*}.js', { parts: true, tokens: true })); +// console.log(scan('foo/**/{bar,*/*}/*.js', { parts: true, tokens: true })); +// console.log(scan('!./foo/*.js')); +// console.log(scan('!./foo/*.js', { parts: true, tokens: true })); diff --git a/lib/parse.js b/lib/parse.js index 34bdc2f..64a9dbc 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -92,7 +92,7 @@ const parse = (input, options) => { START_ANCHOR } = PLATFORM_CHARS; - const globstar = (opts) => { + const globstar = opts => { return `(${capture}(?:(?!${START_ANCHOR}${opts.dot ? DOTS_SLASH : DOT_LITERAL}).)*?)`; }; @@ -260,7 +260,7 @@ const parse = (input, options) => { * Fast paths */ - if (opts.fastpaths !== false && !/(^[*!]|[/()[\]{}"])/.test(input)) { + if (opts.fastpaths !== false && !/(^[*!]|(?!<\\)[/()[\]{}"])/.test(input)) { let backslashes = false; let output = input.replace(REGEX_SPECIAL_CHARS_BACKREF, (m, esc, chars, first, rest, index) => { @@ -444,7 +444,8 @@ const parse = (input, options) => { if (value === '(') { increment('parens'); - push({ type: 'paren', value }); + const output = /(? { continue; } - push({ type: 'paren', value, output: state.parens ? ')' : '\\)' }); + const rest = remaining(); + const special = rest[0] === '?' || rest[0] === '+'; + const paren = `)${special ? rest[0] : ''}\\)?`; + + if (special) { + consume(rest[0], 1); + } + + const output = state.parens ? paren : '\\)'; + push({ type: 'paren', value, output }); decrement('parens'); continue; } @@ -579,7 +589,7 @@ const parse = (input, options) => { state.backtrack = true; } - if (brace.comma !== true && brace.dots !== true) { + if (opts.strictBraces && brace.comma !== true && brace.dots !== true) { const out = state.output.slice(0, brace.outputIndex); const toks = state.tokens.slice(brace.tokensIndex); brace.value = brace.output = '\\{'; @@ -604,6 +614,15 @@ const parse = (input, options) => { if (extglobs.length > 0) { extglobs[extglobs.length - 1].conditions++; } + + // See: https://github.com/micromatch/picomatch/issues/59 + if (opts.strictBraces !== true && braces.length > 0) { + const brace = braces[braces.length - 1]; + if (stack[stack.length - 1] === 'braces') { + brace.comma = true; + } + } + push({ type: 'text', value }); continue; } @@ -829,9 +848,7 @@ const parse = (input, options) => { // strip consecutive `/**/` while (rest.slice(0, 3) === '/**') { const after = input[state.index + 4]; - if (after && after !== '/') { - break; - } + if (after && after !== '/') break; rest = rest.slice(3); consume('/**', 3); } @@ -925,11 +942,9 @@ const parse = (input, options) => { if (prev.type === 'dot') { state.output += NO_DOT_SLASH; prev.output += NO_DOT_SLASH; - } else if (opts.dot === true) { state.output += NO_DOTS_SLASH; prev.output += NO_DOTS_SLASH; - } else { state.output += nodot; prev.output += nodot; @@ -1022,7 +1037,7 @@ parse.fastpaths = (input, options) => { star = `(${star})`; } - const globstar = (opts) => { + const globstar = opts => { if (opts.noglobstar === true) return star; return `(${capture}(?:(?!${START_ANCHOR}${opts.dot ? DOTS_SLASH : DOT_LITERAL}).)*?)`; }; diff --git a/lib/picomatch.js b/lib/picomatch.js index df7438a..4c68f24 100644 --- a/lib/picomatch.js +++ b/lib/picomatch.js @@ -59,7 +59,7 @@ const picomatch = (glob, options, returnState = false) => { let isIgnored = () => false; if (opts.ignore) { - const ignoreOpts = { ...options, ignore: null, onMatch: null, onResult: null }; + const ignoreOpts = { ...options, ...opts.ignoreOptions, ignore: null, onMatch: null, onResult: null }; isIgnored = picomatch(opts.ignore, ignoreOpts, returnState); } @@ -231,44 +231,58 @@ picomatch.parse = (pattern, options) => { picomatch.scan = (input, options) => scan(input, options); /** - * Create a regular expression from a parsed glob pattern. - * - * ```js - * const picomatch = require('picomatch'); - * const state = picomatch.parse('*.js'); - * // picomatch.compileRe(state[, options]); + * Compile a regular expression from the `state` object returned by the + * [parse()](#parse) method. * - * console.log(picomatch.compileRe(state)); - * //=> /^(?:(?!\.)(?=.)[^/]*?\.js)$/ - * ``` - * @param {String} `state` The object returned from the `.parse` method. + * @param {Object} `state` * @param {Object} `options` - * @return {RegExp} Returns a regex created from the given pattern. + * @param {Boolean} `returnOutput` Intended for implementors, this argument allows you to return the raw output from the parser. + * @param {Boolean} `returnState` Adds the state to a `state` property on the returned regex. Useful for implementors and debugging. + * @return {RegExp} * @api public */ -picomatch.compileRe = (parsed, options, returnOutput = false, returnState = false) => { +picomatch.compileRe = (state, options, returnOutput = false, returnState = false) => { if (returnOutput === true) { - return parsed.output; + return state.output; } const opts = options || {}; const prepend = opts.contains ? '' : '^'; const append = opts.contains ? '' : '$'; - let source = `${prepend}(?:${parsed.output})${append}`; - if (parsed && parsed.negated === true) { + let source = `${prepend}(?:${state.output})${append}`; + if (state && state.negated === true) { source = `^(?!${source}).*$`; } const regex = picomatch.toRegex(source, options); if (returnState === true) { - regex.state = parsed; + regex.state = state; } return regex; }; +/** + * Create a regular expression from a parsed glob pattern. + * + * ```js + * const picomatch = require('picomatch'); + * const state = picomatch.parse('*.js'); + * // picomatch.compileRe(state[, options]); + * + * console.log(picomatch.compileRe(state)); + * //=> /^(?:(?!\.)(?=.)[^/]*?\.js)$/ + * ``` + * @param {String} `state` The object returned from the `.parse` method. + * @param {Object} `options` + * @param {Boolean} `returnOutput` Implementors may use this argument to return the compiled output, instead of a regular expression. This is not exposed on the options to prevent end-users from mutating the result. + * @param {Boolean} `returnState` Implementors may use this argument to return the state from the parsed glob with the returned regular expression. + * @return {RegExp} Returns a regex created from the given pattern. + * @api public + */ + picomatch.makeRe = (input, options, returnOutput = false, returnState = false) => { if (!input || typeof input !== 'string') { throw new TypeError('Expected a non-empty string'); diff --git a/lib/scan.js b/lib/scan.js index f291890..3fcaf7f 100644 --- a/lib/scan.js +++ b/lib/scan.js @@ -188,16 +188,38 @@ const scan = (input, options) => { if (code === CHAR_RIGHT_PARENTHESES) { isGlob = token.isGlob = true; - finished = true; break; } } continue; } + break; } } + if (opts.noparen !== true && code === CHAR_LEFT_PARENTHESES) { + while (eos() !== true && (code = advance())) { + if (code === CHAR_BACKWARD_SLASH) { + backslashes = token.backslashes = true; + code = advance(); + continue; + } + + if (code === CHAR_RIGHT_PARENTHESES) { + isGlob = token.isGlob = true; + break; + } + } + + if (scanToEnd === true) { + continue; + } + + finished = isGlob; + break; + } + if (code === CHAR_ASTERISK) { if (prev === CHAR_ASTERISK) isGlobstar = token.isGlobstar = true; isGlob = token.isGlob = true; @@ -246,26 +268,6 @@ const scan = (input, options) => { continue; } - if (opts.noparen !== true && code === CHAR_LEFT_PARENTHESES) { - while (eos() !== true && (code = advance())) { - if (code === CHAR_BACKWARD_SLASH) { - backslashes = token.backslashes = true; - code = advance(); - continue; - } - - if (code === CHAR_RIGHT_PARENTHESES) { - isGlob = token.isGlob = true; - finished = true; - - if (scanToEnd === true) { - continue; - } - break; - } - } - } - if (isGlob === true) { finished = true; @@ -332,33 +334,43 @@ const scan = (input, options) => { if (opts.tokens === true) { state.maxDepth = 0; - if (!isPathSeparator(code)) { - tokens.push(token); - } + tokens.push(token); state.tokens = tokens; + parts.push(token.value); } if (opts.parts === true || opts.tokens === true) { let prevIndex; - for (let idx = 0; idx < slashes.length; idx++) { + for (let i = 0; i < slashes.length; i++) { const n = prevIndex ? prevIndex + 1 : start; - const i = slashes[idx]; - const value = input.slice(n, i); + const slashIndex = slashes[i]; + + if (n === 0 && slashIndex === 0 && slashes.length === 1) { + parts.push('', input.slice(1)); + break; + } + + let value = input.slice(n, slashIndex); + if (value[0] === '/') value = value.slice(1); + if (opts.tokens) { - if (idx === 0 && start !== 0) { - tokens[idx].isPrefix = true; - tokens[idx].value = prefix; + if (i === 0 && start !== 0) { + tokens[i].isPrefix = true; + tokens[i].value = prefix; } else { - tokens[idx].value = value; + tokens[i].value = value; } - depth(tokens[idx]); - state.maxDepth += tokens[idx].depth; + + depth(tokens[i]); + state.maxDepth += tokens[i].depth; } - if (idx !== 0 || value !== '') { + + if (i !== 0 || prefix !== './') { parts.push(value); } - prevIndex = i; + + prevIndex = slashIndex; } if (prevIndex && prevIndex + 1 < input.length) { @@ -370,6 +382,9 @@ const scan = (input, options) => { depth(tokens[tokens.length - 1]); state.maxDepth += tokens[tokens.length - 1].depth; } + + } else if ((str && parts.length === 0) || input[input.length - 1] === '/') { + parts.push(''); } state.slashes = slashes; diff --git a/test/api.picomatch.js b/test/api.picomatch.js index 0d17dba..fc48728 100644 --- a/test/api.picomatch.js +++ b/test/api.picomatch.js @@ -5,8 +5,7 @@ const picomatch = require('..'); const { isMatch } = picomatch; const assertTokens = (actual, expected) => { - const keyValuePairs = actual.map((token) => [token.type, token.value]); - + const keyValuePairs = actual.map(token => [token.type, token.value]); assert.deepStrictEqual(keyValuePairs, expected); }; @@ -214,7 +213,7 @@ describe('picomatch', () => { assert(!isMatch('zzjs', '*z.js')); }); - it('issue #24', () => { + it('issue #24 - should match zero or more directories', () => { assert(!isMatch('a/b/c/d/', 'a/b/**/f')); assert(isMatch('a', 'a/**')); assert(isMatch('a', '**')); @@ -249,6 +248,7 @@ describe('picomatch', () => { assert(!isMatch('deep/foo/bar/baz', '**/bar/*/')); assert(!isMatch('deep/foo/bar/baz/', '**/bar/*', { strictSlashes: true })); assert(isMatch('deep/foo/bar/baz/', '**/bar/*')); + assert(isMatch('deep/foo/bar/baz', '**/bar/*')); assert(isMatch('foo', 'foo/**')); assert(isMatch('deep/foo/bar/baz/', '**/bar/*{,/}')); assert(isMatch('a/b/j/c/z/x.md', 'a/**/j/**/z/*.md')); diff --git a/test/api.scan.js b/test/api.scan.js index d23731b..1296990 100644 --- a/test/api.scan.js +++ b/test/api.scan.js @@ -15,8 +15,18 @@ const both = (...args) => { * and both libraries use path.dirname. Picomatch does not. */ -describe('picomatch', () => { - describe('.scan', () => { +describe('.scan', () => { + describe('when', () => { + it('should ', () => { + assert.deepStrictEqual(scan('/a', { parts: true }).parts, ['', 'a']); + assert.deepStrictEqual(scan('/a/b', { parts: true }).parts, ['', 'a', 'b']); + assert.deepStrictEqual(scan('/a/b/', { parts: true }).parts, ['', 'a', 'b', '']); + assert.deepStrictEqual(scan('(!(b/a))', { parts: true }).parts, ['']); + assert.deepStrictEqual(scan('(a|b)/c', { parts: true }).parts, ['(a|b)', 'c']); + }); + }); + + describe('base', () => { it('should get the "base" and "glob" from a pattern', () => { assert.deepStrictEqual(both('foo/bar'), ['foo/bar', '']); assert.deepStrictEqual(both('foo/@bar'), ['foo/@bar', '']); diff --git a/test/dotfiles.js b/test/dotfiles.js index e72e0f6..f90abb8 100644 --- a/test/dotfiles.js +++ b/test/dotfiles.js @@ -224,7 +224,7 @@ describe('dotfiles', () => { assert(isMatch('abc/../abc', '*/../*')); }); - it('should not match double dots when not defined in pattern', async() => { + it('should not match double dots when not defined in pattern', async () => { assert(!isMatch('../abc', '**/*')); assert(!isMatch('../abc', '**/**/**')); assert(!isMatch('../abc', '**/**/abc')); @@ -291,7 +291,7 @@ describe('dotfiles', () => { assert(!isMatch('abc/abc/..', 'abc/*/**/*', { strictSlashes: true })); }); - it('should not match single exclusive dots when not defined in pattern', async() => { + it('should not match single exclusive dots when not defined in pattern', async () => { assert(!isMatch('.', '**')); assert(!isMatch('abc/./abc', '**')); assert(!isMatch('abc/abc/.', '**')); diff --git a/test/extglobs.js b/test/extglobs.js index 29bcb06..979bb57 100644 --- a/test/extglobs.js +++ b/test/extglobs.js @@ -25,7 +25,7 @@ describe('extglobs', () => { }); it('should not convert capture groups to extglobs', () => { - assert.strictEqual(makeRe('c!(?:foo)?z').source, '^(?:c!(?:foo)?z)$'); + assert.strictEqual(makeRe('c!(?:foo)?z').source, '^(?:c!\\(?(?:foo)?\\)?z)$'); assert(!isMatch('c/z', 'c!(?:foo)?z')); assert(isMatch('c!fooz', 'c!(?:foo)?z')); assert(isMatch('c!z', 'c!(?:foo)?z')); diff --git a/test/malicious.js b/test/malicious.js index 33cf6b8..20a2f86 100644 --- a/test/malicious.js +++ b/test/malicious.js @@ -13,7 +13,9 @@ describe('handling of potential regex exploits', () => { if (process.platform !== 'win32') { assert(isMatch('\\A', `${repeat(65500)}A`), 'within the limits, and valid match'); } - assert(isMatch('A', `!${repeat(65500)}A`), 'within the limits, and valid match'); + assert(isMatch('A', `${repeat(65499)}A`), 'within the limits, and invalid match'); + assert(!isMatch('A', `!${repeat(65499)}A`), 'within the limits, and invalid match'); + assert(!isMatch('A', `${repeat(65500)}A`), 'within the limits, and valid match'); assert(isMatch('A', `!(${repeat(65500)}A)`), 'within the limits, and valid match'); assert(!isMatch('A', `[!(${repeat(65500)}A`), 'within the limits, but invalid regex'); }); diff --git a/test/parens.js b/test/parens.js index d73bf63..9398e43 100644 --- a/test/parens.js +++ b/test/parens.js @@ -15,6 +15,18 @@ describe('parens (non-extglobs)', () => { assert(isMatch('aaabbb', '(a|b)*')); }); + it('should match literal parens', () => { + assert(isMatch('(a)', '(a)*')); + assert(isMatch('(az)', '(a)*')); + assert(!isMatch('(zz)', '(a)*')); + assert(isMatch('(ab)', '(a|b)*')); + assert(isMatch('(abc)', '(a|b)*')); + assert(isMatch('(aa)', '(a)*')); + assert(isMatch('(aaab)', '(a|b)*')); + assert(isMatch('(aaabbb)', '(a|b)*')); + assert(!isMatch('(zaabbb)', '(a|b)*')); + }); + it('should not match slashes with single stars', () => { assert(!isMatch('a/b', '(a)*')); assert(!isMatch('a/b', '(a|b)*')); diff --git a/test/special-characters.js b/test/special-characters.js index 7ff8920..8ff260a 100644 --- a/test/special-characters.js +++ b/test/special-characters.js @@ -232,7 +232,7 @@ describe('special characters', () => { assert(isMatch('foo(bar)baz', 'foo*baz')); }); - it('should match literal parens with brackets', async() => { + it('should match literal parens with brackets', async () => { assert(isMatch('foo(bar)baz', 'foo[bar()]+baz')); }); diff --git a/test/stars.js b/test/stars.js index 910b9f2..12bbd0c 100644 --- a/test/stars.js +++ b/test/stars.js @@ -360,6 +360,8 @@ describe('stars', () => { it('should optionally match trailing slashes with braces', () => { assert(isMatch('foo', '**/*')); + assert(isMatch('foo', '*{,/}')); + assert(isMatch('foo/', '*{,/}')); assert(isMatch('foo', '**/*{,/}')); assert(isMatch('foo/', '**/*{,/}')); assert(isMatch('foo/bar', '**/*{,/}'));