Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .verb.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,19 @@ console.log(braces.expand('a{b}c'));
console.log(braces('a/{b,c}/d', { maxLength: 3 })); //=> throws an error
```

### options.maxDepth

**Type**: `Number`

**Default**: `256`

**Description**: Limit how deeply braces and parentheses can be nested. Anything nested deeper than the limit is treated as literal text, so very deep patterns can't exhaust the call stack. Values higher than the default are capped at the default.

```js
console.log(braces.expand('{{a,b},c}', { maxDepth: 1 }));
//=> [ '{a,b}', 'c' ]
```

### options.expand

**Type**: `Boolean`
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,19 @@ console.log(braces.expand('a{b}c'));
console.log(braces('a/{b,c}/d', { maxLength: 3 })); //=> throws an error
```

### options.maxDepth

**Type**: `Number`

**Default**: `256`

**Description**: Limit how deeply braces and parentheses can be nested. Anything nested deeper than the limit is treated as literal text, so very deep patterns can't exhaust the call stack. Values higher than the default are capped at the default.

```js
console.log(braces.expand('{{a,b},c}', { maxDepth: 1 }));
//=> [ '{a,b}', 'c' ]
```

### options.expand

**Type**: `Boolean`
Expand Down
1 change: 1 addition & 0 deletions lib/constants.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

module.exports = {
MAX_DEPTH: 256,
MAX_LENGTH: 10000,

// Digits
Expand Down
17 changes: 17 additions & 0 deletions lib/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const stringify = require('./stringify');
*/

const {
MAX_DEPTH,
MAX_LENGTH,
CHAR_BACKSLASH, /* \ */
CHAR_BACKTICK, /* ` */
Expand Down Expand Up @@ -39,6 +40,7 @@ const parse = (input, options = {}) => {
throw new SyntaxError(`Input length (${input.length}), exceeds max characters (${max})`);
}

const maxDepth = Number.isFinite(opts.maxDepth) && opts.maxDepth >= 0 ? Math.min(MAX_DEPTH, opts.maxDepth) : MAX_DEPTH;
const ast = { type: 'root', input, nodes: [] };
const stack = [ast];
let block = ast;
Expand All @@ -47,6 +49,9 @@ const parse = (input, options = {}) => {
const length = input.length;
let index = 0;
let depth = 0;
// Nesting beyond maxDepth is kept as literal text so every recursive walker (compile, expand,
// stringify, flatten) sees a bounded tree and callers never get a RangeError.
let literalDepth = 0;
let value;

/**
Expand Down Expand Up @@ -77,6 +82,18 @@ const parse = (input, options = {}) => {
block = stack[stack.length - 1];
value = advance();

if (literalDepth > 0 || ((value === CHAR_LEFT_CURLY_BRACE || value === CHAR_LEFT_PARENTHESES) && stack.length > maxDepth)) {
if (value === CHAR_LEFT_CURLY_BRACE || value === CHAR_LEFT_PARENTHESES) {
literalDepth++;
} else if (value === CHAR_RIGHT_CURLY_BRACE || value === CHAR_RIGHT_PARENTHESES) {
literalDepth--;
Comment thread
stefanstankovic1 marked this conversation as resolved.
Resolved
} else if (value === CHAR_BACKSLASH) {
value += index < length ? advance() : '';
}
push({ type: 'text', value });
continue;
}

/**
* Invalid chars
*/
Expand Down
55 changes: 55 additions & 0 deletions test/nesting-depth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use strict';

require('mocha');
const assert = require('assert').strict;
const braces = require('..');

const nested = (open, close, depth) => open.repeat(depth) + 'a,b' + close.repeat(depth);

describe('nesting depth', () => {
describe('deeply nested patterns', () => {
// 4,995 levels stays under the default maxLength of 10,000
const deepBraces = nested('{', '}', 4995);
const deepParens = nested('(', ')', 4995);

it('should not overflow the stack when compiling', () => {
assert.equal(typeof braces(deepBraces)[0], 'string');
assert.equal(typeof braces(deepParens)[0], 'string');
});

it('should not overflow the stack when expanding', () => {
assert.ok(Array.isArray(braces.expand(deepBraces)));
assert.ok(Array.isArray(braces.expand(deepParens)));
});

it('should not overflow the stack when stringifying', () => {
assert.equal(braces.stringify(deepBraces), deepBraces);
assert.equal(braces.stringify(deepParens), deepParens);
});
});

describe('options.maxDepth', () => {
it('should not change results for patterns within the limit', () => {
assert.deepEqual(braces.expand('a{b,{c,{d,e}}}f'), ['abf', 'acf', 'adf', 'aef']);
});

it('should treat nesting beyond the limit as literal text', () => {
assert.deepEqual(braces.expand('{{a,b},c}', { maxDepth: 1 }), ['{a,b}', 'c']);
assert.deepEqual(braces.expand('{{a,b},c}', { maxDepth: 2 }), ['a', 'b', 'c']);
});

it('should keep escaped characters beyond the limit', () => {
assert.deepEqual(braces.expand('{{a,\\}b},c}', { maxDepth: 1 }), ['{a,\\}b}', 'c']);
});

it('should not allow the limit to be raised above the default', () => {
assert.ok(Array.isArray(braces.expand(nested('{', '}', 4995), { maxDepth: Infinity })));
});

it('should fall back to the default when the value is invalid', () => {
for (const maxDepth of [NaN, -1, '1']) {
assert.deepEqual(braces.expand('{{a,b},c}', { maxDepth }), ['a', 'b', 'c']);
}
});
});
});