source: node_modules/brace-expansion/dist/esm/index.js@ 06ebe74

finki-main main
Last change on this file since 06ebe74 was 33517cc, checked in by Klimentina Efremova <klimentina08642@…>, 11 days ago

Turned database from SQLite to PostgressSQL, updated database changes from Phase 1 and 2

  • Property mode set to 100644
File size: 13.4 KB
Line 
1import { balanced } from 'balanced-match';
2const escSlash = '\0SLASH' + Math.random() + '\0';
3const escOpen = '\0OPEN' + Math.random() + '\0';
4const escClose = '\0CLOSE' + Math.random() + '\0';
5const escComma = '\0COMMA' + Math.random() + '\0';
6const escPeriod = '\0PERIOD' + Math.random() + '\0';
7const escSlashPattern = new RegExp(escSlash, 'g');
8const escOpenPattern = new RegExp(escOpen, 'g');
9const escClosePattern = new RegExp(escClose, 'g');
10const escCommaPattern = new RegExp(escComma, 'g');
11const escPeriodPattern = new RegExp(escPeriod, 'g');
12const slashPattern = /\\\\/g;
13const openPattern = /\\{/g;
14const closePattern = /\\}/g;
15const commaPattern = /\\,/g;
16const periodPattern = /\\\./g;
17export const EXPANSION_MAX = 100_000;
18// `EXPANSION_MAX` caps the *number* of expansions, but not their length. An
19// input like `'{a,b}'.repeat(1500)` stays under that count - its output is
20// truncated to 100k results - while making every result ~1500 characters
21// long. The result set, and the intermediate arrays built while combining
22// brace sets, then grow large enough to exhaust memory and crash the process
23// (CVE-2026-14257). `EXPANSION_MAX_LENGTH` bounds the total number of
24// characters the accumulator may hold at any point, so memory stays flat no
25// matter how many brace groups are chained. The limit sits well above any
26// realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
27// characters) so legitimate input is unaffected.
28export const EXPANSION_MAX_LENGTH = 4_000_000;
29// `expand_` recurses once per level of brace *nesting* - both when expanding a
30// set's comma members and when re-wrapping a set whose body is a single part.
31// The CVE-2026-14257 fix made the *tail* iterative (recursion on `m.post`, one
32// level per chained group), which left nesting depth unbounded: about 3,100
33// levels of `{{{...a,b...}}}` - only ~6KB of input - exhausted the native stack
34// and crashed the process. `EXPANSION_MAX_DEPTH` bounds how deep the parser
35// will follow nesting. It sits far above any realistic pattern and well below
36// the depth at which the stack runs out.
37export const EXPANSION_MAX_DEPTH = 1_000;
38// Bash keeps a quirk where a brace group followed by a comma set still expands
39// (`{a},b}`). The parser implements it by rewriting the string and restarting
40// the scan, absorbing one `}` per pass. `n` trailing braces therefore cost `n`
41// full passes over a string that itself grows by one `escClose` sentinel each
42// time - quadratic in `n`, with a ~26x constant from the sentinel's length.
43// 128KB of `'{a}' + '}'.repeat(n) + ',z}'` blocked the event loop for 27
44// seconds to produce two results. `EXPANSION_MAX_REWRITES` bounds how many
45// times the scan may restart. Real `{a},b}` input needs a handful.
46export const EXPANSION_MAX_REWRITES = 1_000;
47function numeric(str) {
48 return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
49}
50function escapeBraces(str) {
51 return str
52 .replace(slashPattern, escSlash)
53 .replace(openPattern, escOpen)
54 .replace(closePattern, escClose)
55 .replace(commaPattern, escComma)
56 .replace(periodPattern, escPeriod);
57}
58function unescapeBraces(str) {
59 return str
60 .replace(escSlashPattern, '\\')
61 .replace(escOpenPattern, '{')
62 .replace(escClosePattern, '}')
63 .replace(escCommaPattern, ',')
64 .replace(escPeriodPattern, '.');
65}
66// Like `target.push(...items)` but doesn't overflow the stack
67function pushAll(target, items) {
68 for (let i = 0; i < items.length; i++) {
69 target.push(items[i]);
70 }
71}
72/**
73 * Basically just str.split(","), but handling cases
74 * where we have nested braced sections, which should be
75 * treated as individual members, like {a,{b,c},d}
76 */
77function parseCommaParts(str) {
78 const parts = [];
79 // Walk the brace groups iteratively. Recursing on `post` once per group let a
80 // chain of them exhaust the stack - the parsing-side counterpart to
81 // the `expand_` overflow fixed for CVE-2026-14257, and not something `max` or
82 // `maxLength` can bound, since it happens before expansion.
83 //
84 // The part the next chunk continues
85 let carry = '';
86 for (;;) {
87 const m = balanced('{', '}', str);
88 if (!m) {
89 const tail = str.split(',');
90 tail[0] = carry + tail[0];
91 pushAll(parts, tail);
92 return parts;
93 }
94 const { pre, body, post } = m;
95 const p = pre.split(',');
96 p[0] = carry + p[0];
97 p[p.length - 1] += '{' + body + '}';
98 if (!post.length) {
99 pushAll(parts, p);
100 return parts;
101 }
102 carry = p.pop();
103 pushAll(parts, p);
104 str = post;
105 }
106}
107export function expand(str, options = {}) {
108 if (!str) {
109 return [];
110 }
111 const { max = EXPANSION_MAX, maxLength = EXPANSION_MAX_LENGTH, maxDepth = EXPANSION_MAX_DEPTH, maxRewrites = EXPANSION_MAX_REWRITES, } = options;
112 // I don't know why Bash 4.3 does this, but it does.
113 // Anything starting with {} will have the first two bytes preserved
114 // but *only* at the top level, so {},a}b will not expand to anything,
115 // but a{},b}c will be expanded to [a}c,abc].
116 // One could argue that this is a bug in Bash, but since the goal of
117 // this module is to match Bash's rules, we escape a leading {}
118 if (str.slice(0, 2) === '{}') {
119 str = '\\{\\}' + str.slice(2);
120 }
121 return expand_(escapeBraces(str), max, maxLength, maxDepth, 0, maxRewrites, true).map(unescapeBraces);
122}
123function embrace(str) {
124 return '{' + str + '}';
125}
126function isPadded(el) {
127 return /^-?0\d/.test(el);
128}
129function lte(i, y) {
130 return i <= y;
131}
132function gte(i, y) {
133 return i >= y;
134}
135// Build `{ acc[a] + pre + values[v] }` for every combination, capping the
136// number of results at `max` and the total number of characters at `maxLength`.
137// This is the one place output grows, so bounding it here keeps the single
138// accumulator - and therefore memory - flat regardless of how many brace groups
139// are combined (CVE-2026-14257).
140function combine(acc, pre, values, max, maxLength, dropEmpties) {
141 const out = [];
142 let length = 0;
143 for (let a = 0; a < acc.length; a++) {
144 for (let v = 0; v < values.length; v++) {
145 if (out.length >= max)
146 return out;
147 const expansion = acc[a] + pre + values[v];
148 // Bash drops empty results at the top level. Skip them before they count
149 // against `max`, so `max` bounds the number of *kept* results.
150 if (dropEmpties && !expansion)
151 continue;
152 if (length + expansion.length > maxLength)
153 return out;
154 out.push(expansion);
155 length += expansion.length;
156 }
157 }
158 return out;
159}
160// The expansion values of a single numeric (`1..5`) or alphabetic (`a..e..2`)
161// sequence body.
162function expandSequence(body, isAlphaSequence, max, maxLength) {
163 const n = body.split(/\.\./);
164 const N = [];
165 // A sequence body always splits into two or three parts, but the compiler
166 // can't know that.
167 /* c8 ignore start */
168 if (n[0] === undefined || n[1] === undefined) {
169 return N;
170 }
171 /* c8 ignore stop */
172 const x = numeric(n[0]);
173 const y = numeric(n[1]);
174 const width = Math.max(n[0].length, n[1].length);
175 let incr = n.length === 3 && n[2] !== undefined ?
176 Math.max(Math.abs(numeric(n[2])), 1)
177 : 1;
178 let test = lte;
179 const reverse = y < x;
180 if (reverse) {
181 incr *= -1;
182 test = gte;
183 }
184 const pad = n.some(isPadded);
185 let length = 0;
186 for (let i = x; test(i, y) && N.length < max; i += incr) {
187 let c;
188 if (isAlphaSequence) {
189 c = String.fromCharCode(i);
190 if (c === '\\') {
191 c = '';
192 }
193 }
194 else {
195 c = String(i);
196 if (pad) {
197 const need = width - c.length;
198 if (need > 0) {
199 const z = new Array(need + 1).join('0');
200 if (i < 0) {
201 c = '-' + z + c.slice(1);
202 }
203 else {
204 c = z + c;
205 }
206 }
207 }
208 }
209 if (length + c.length > maxLength)
210 break;
211 N.push(c);
212 length += c.length;
213 }
214 return N;
215}
216function expand_(str, max, maxLength, maxDepth, depth, maxRewrites, isTop) {
217 // Too deeply nested to keep following: treat the rest as literal, the same
218 // way a group that cannot expand is already handled. Truncating rather than
219 // throwing keeps `expand` total, matching `max` and `maxLength`.
220 if (depth > maxDepth) {
221 return [str];
222 }
223 // Consume the string's top-level brace groups left to right, threading a
224 // running set of combined prefixes (`acc`). Expanding the tail iteratively -
225 // rather than recursing on `m.post` once per group - keeps the native stack
226 // depth constant, so deeply chained input (`'{a,b}'.repeat(3000)`) can no
227 // longer overflow the stack, and leaves a single accumulator whose size
228 // `maxLength` bounds directly (CVE-2026-14257).
229 let acc = [''];
230 // Bash drops empty results, but only when the *first* top-level group is a
231 // comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
232 // is on the final strings, so it is applied to whichever `combine` produces
233 // them (the one with no brace set left in the tail).
234 // How many times the `{a},b}` rewrite below has restarted the scan. Each pass
235 // re-reads the whole string, so leaving this unbounded is quadratic.
236 let rewrites = 0;
237 let dropEmpties = false;
238 let firstGroup = true;
239 for (;;) {
240 const m = balanced('{', '}', str);
241 // No brace set left: the rest of the string is literal.
242 if (!m) {
243 return combine(acc, str, [''], max, maxLength, dropEmpties);
244 }
245 // no need to expand pre, since it is guaranteed to be free of brace-sets
246 const pre = m.pre;
247 if (/\$$/.test(pre)) {
248 acc = combine(acc, pre + '{' + m.body + '}', [''], max, maxLength, dropEmpties && !m.post.length);
249 firstGroup = false;
250 if (!m.post.length)
251 break;
252 str = m.post;
253 continue;
254 }
255 const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
256 const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
257 const isSequence = isNumericSequence || isAlphaSequence;
258 const isOptions = m.body.indexOf(',') >= 0;
259 if (!isSequence && !isOptions) {
260 // {a},b}
261 if (rewrites < maxRewrites && m.post.match(/,(?!,).*\}/)) {
262 rewrites++;
263 str = m.pre + '{' + m.body + escClose + m.post;
264 isTop = true;
265 continue;
266 }
267 // Nothing here expands, so the whole remaining string is literal.
268 return combine(acc, pre + '{' + m.body + '}' + m.post, [''], max, maxLength, dropEmpties);
269 }
270 if (firstGroup) {
271 dropEmpties = isTop && !isSequence;
272 firstGroup = false;
273 }
274 let values;
275 if (isSequence) {
276 values = expandSequence(m.body, isAlphaSequence, max, maxLength);
277 }
278 else {
279 let n = parseCommaParts(m.body);
280 if (n.length === 1 && n[0] !== undefined) {
281 // x{{a,b}}y ==> x{a}y x{b}y
282 n = expand_(n[0], max, maxLength, maxDepth, depth + 1, maxRewrites, false).map(embrace);
283 //XXX is this necessary? Can't seem to hit it in tests.
284 /* c8 ignore start */
285 if (n.length === 1) {
286 acc = combine(acc, pre + n[0], [''], max, maxLength, dropEmpties && !m.post.length);
287 if (!m.post.length)
288 break;
289 str = m.post;
290 continue;
291 }
292 /* c8 ignore stop */
293 }
294 // Values that `combine` is going to drop as empty produce no result, so
295 // they must not count against `max` - otherwise `{a,,b}` with `max: 2`
296 // would stop at `['a', '']` and yield one result instead of two. Skipping
297 // them outright keeps `values` bounded while leaving `max` a bound on
298 // *kept* results.
299 let dropsEmpties = dropEmpties && !m.post.length && !pre;
300 for (let d = 0; dropsEmpties && d < acc.length; d++) {
301 if (acc[d]) {
302 dropsEmpties = false;
303 }
304 }
305 values = [];
306 let valuesLength = 0;
307 outer: for (let j = 0; j < n.length; j++) {
308 const expanded = expand_(n[j], max, maxLength, maxDepth, depth + 1, maxRewrites, false);
309 for (let k = 0; k < expanded.length; k++) {
310 const v = expanded[k];
311 if (dropsEmpties && !v)
312 continue;
313 if (values.length >= max ||
314 valuesLength + v.length > maxLength) {
315 break outer;
316 }
317 values.push(v);
318 valuesLength += v.length;
319 }
320 }
321 }
322 acc = combine(acc, pre, values, max, maxLength, dropEmpties && !m.post.length);
323 if (!m.post.length)
324 break;
325 str = m.post;
326 }
327 return acc;
328}
329//# sourceMappingURL=index.js.map
Note: See TracBrowser for help on using the repository browser.