6de2455917
- Added GLOBAL_MARKETS_TITLE to all translation files - Updated footer with 12 markets (4 active + 8 upcoming) - Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi - Built and deployed to production - CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
263 lines
9.8 KiB
JavaScript
263 lines
9.8 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.EXPANSION_MAX_LENGTH = exports.EXPANSION_MAX = void 0;
|
|
exports.expand = expand;
|
|
const balanced_match_1 = require("balanced-match");
|
|
const escSlash = '\0SLASH' + Math.random() + '\0';
|
|
const escOpen = '\0OPEN' + Math.random() + '\0';
|
|
const escClose = '\0CLOSE' + Math.random() + '\0';
|
|
const escComma = '\0COMMA' + Math.random() + '\0';
|
|
const escPeriod = '\0PERIOD' + Math.random() + '\0';
|
|
const escSlashPattern = new RegExp(escSlash, 'g');
|
|
const escOpenPattern = new RegExp(escOpen, 'g');
|
|
const escClosePattern = new RegExp(escClose, 'g');
|
|
const escCommaPattern = new RegExp(escComma, 'g');
|
|
const escPeriodPattern = new RegExp(escPeriod, 'g');
|
|
const slashPattern = /\\\\/g;
|
|
const openPattern = /\\{/g;
|
|
const closePattern = /\\}/g;
|
|
const commaPattern = /\\,/g;
|
|
const periodPattern = /\\\./g;
|
|
exports.EXPANSION_MAX = 100_000;
|
|
// `EXPANSION_MAX` caps the *number* of expansions, but not their length. An
|
|
// input like `'{a,b}'.repeat(1500)` stays under that count - its output is
|
|
// truncated to 100k results - while making every result ~1500 characters
|
|
// long. The result set, and the intermediate arrays built while combining
|
|
// brace sets, then grow large enough to exhaust memory and crash the process
|
|
// (CVE-2026-14257). `EXPANSION_MAX_LENGTH` bounds the total number of
|
|
// characters the accumulator may hold at any point, so memory stays flat no
|
|
// matter how many brace groups are chained. The limit sits well above any
|
|
// realistic expansion (100k results hitting `EXPANSION_MAX` measure ~1M
|
|
// characters) so legitimate input is unaffected.
|
|
exports.EXPANSION_MAX_LENGTH = 4_000_000;
|
|
function numeric(str) {
|
|
return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
|
|
}
|
|
function escapeBraces(str) {
|
|
return str
|
|
.replace(slashPattern, escSlash)
|
|
.replace(openPattern, escOpen)
|
|
.replace(closePattern, escClose)
|
|
.replace(commaPattern, escComma)
|
|
.replace(periodPattern, escPeriod);
|
|
}
|
|
function unescapeBraces(str) {
|
|
return str
|
|
.replace(escSlashPattern, '\\')
|
|
.replace(escOpenPattern, '{')
|
|
.replace(escClosePattern, '}')
|
|
.replace(escCommaPattern, ',')
|
|
.replace(escPeriodPattern, '.');
|
|
}
|
|
/**
|
|
* Basically just str.split(","), but handling cases
|
|
* where we have nested braced sections, which should be
|
|
* treated as individual members, like {a,{b,c},d}
|
|
*/
|
|
function parseCommaParts(str) {
|
|
if (!str) {
|
|
return [''];
|
|
}
|
|
const parts = [];
|
|
const m = (0, balanced_match_1.balanced)('{', '}', str);
|
|
if (!m) {
|
|
return str.split(',');
|
|
}
|
|
const { pre, body, post } = m;
|
|
const p = pre.split(',');
|
|
p[p.length - 1] += '{' + body + '}';
|
|
const postParts = parseCommaParts(post);
|
|
if (post.length) {
|
|
;
|
|
p[p.length - 1] += postParts.shift();
|
|
p.push.apply(p, postParts);
|
|
}
|
|
parts.push.apply(parts, p);
|
|
return parts;
|
|
}
|
|
function expand(str, options = {}) {
|
|
if (!str) {
|
|
return [];
|
|
}
|
|
const { max = exports.EXPANSION_MAX, maxLength = exports.EXPANSION_MAX_LENGTH } = options;
|
|
// I don't know why Bash 4.3 does this, but it does.
|
|
// Anything starting with {} will have the first two bytes preserved
|
|
// but *only* at the top level, so {},a}b will not expand to anything,
|
|
// but a{},b}c will be expanded to [a}c,abc].
|
|
// One could argue that this is a bug in Bash, but since the goal of
|
|
// this module is to match Bash's rules, we escape a leading {}
|
|
if (str.slice(0, 2) === '{}') {
|
|
str = '\\{\\}' + str.slice(2);
|
|
}
|
|
return expand_(escapeBraces(str), max, maxLength, true).map(unescapeBraces);
|
|
}
|
|
function embrace(str) {
|
|
return '{' + str + '}';
|
|
}
|
|
function isPadded(el) {
|
|
return /^-?0\d/.test(el);
|
|
}
|
|
function lte(i, y) {
|
|
return i <= y;
|
|
}
|
|
function gte(i, y) {
|
|
return i >= y;
|
|
}
|
|
// Build `{ acc[a] + pre + values[v] }` for every combination, capping the
|
|
// number of results at `max` and the total number of characters at `maxLength`.
|
|
// This is the one place output grows, so bounding it here keeps the single
|
|
// accumulator - and therefore memory - flat regardless of how many brace groups
|
|
// are combined (CVE-2026-14257).
|
|
function combine(acc, pre, values, max, maxLength, dropEmpties) {
|
|
const out = [];
|
|
let length = 0;
|
|
for (let a = 0; a < acc.length; a++) {
|
|
for (let v = 0; v < values.length; v++) {
|
|
if (out.length >= max)
|
|
return out;
|
|
const expansion = acc[a] + pre + values[v];
|
|
// Bash drops empty results at the top level. Skip them before they count
|
|
// against `max`, so `max` bounds the number of *kept* results.
|
|
if (dropEmpties && !expansion)
|
|
continue;
|
|
if (length + expansion.length > maxLength)
|
|
return out;
|
|
out.push(expansion);
|
|
length += expansion.length;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
// The expansion values of a single numeric (`1..5`) or alphabetic (`a..e..2`)
|
|
// sequence body.
|
|
function expandSequence(body, isAlphaSequence, max) {
|
|
const n = body.split(/\.\./);
|
|
const N = [];
|
|
// A sequence body always splits into two or three parts, but the compiler
|
|
// can't know that.
|
|
/* c8 ignore start */
|
|
if (n[0] === undefined || n[1] === undefined) {
|
|
return N;
|
|
}
|
|
/* c8 ignore stop */
|
|
const x = numeric(n[0]);
|
|
const y = numeric(n[1]);
|
|
const width = Math.max(n[0].length, n[1].length);
|
|
let incr = n.length === 3 && n[2] !== undefined ?
|
|
Math.max(Math.abs(numeric(n[2])), 1)
|
|
: 1;
|
|
let test = lte;
|
|
const reverse = y < x;
|
|
if (reverse) {
|
|
incr *= -1;
|
|
test = gte;
|
|
}
|
|
const pad = n.some(isPadded);
|
|
for (let i = x; test(i, y) && N.length < max; i += incr) {
|
|
let c;
|
|
if (isAlphaSequence) {
|
|
c = String.fromCharCode(i);
|
|
if (c === '\\') {
|
|
c = '';
|
|
}
|
|
}
|
|
else {
|
|
c = String(i);
|
|
if (pad) {
|
|
const need = width - c.length;
|
|
if (need > 0) {
|
|
const z = new Array(need + 1).join('0');
|
|
if (i < 0) {
|
|
c = '-' + z + c.slice(1);
|
|
}
|
|
else {
|
|
c = z + c;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
N.push(c);
|
|
}
|
|
return N;
|
|
}
|
|
function expand_(str, max, maxLength, isTop) {
|
|
// Consume the string's top-level brace groups left to right, threading a
|
|
// running set of combined prefixes (`acc`). Expanding the tail iteratively -
|
|
// rather than recursing on `m.post` once per group - keeps the native stack
|
|
// depth constant, so deeply chained input (`'{a,b}'.repeat(3000)`) can no
|
|
// longer overflow the stack, and leaves a single accumulator whose size
|
|
// `maxLength` bounds directly (CVE-2026-14257).
|
|
let acc = [''];
|
|
// Bash drops empty results, but only when the *first* top-level group is a
|
|
// comma set - a sequence like `{a..\}` may legitimately yield ''. The drop
|
|
// is on the final strings, so it is applied to whichever `combine` produces
|
|
// them (the one with no brace set left in the tail).
|
|
let dropEmpties = false;
|
|
let firstGroup = true;
|
|
for (;;) {
|
|
const m = (0, balanced_match_1.balanced)('{', '}', str);
|
|
// No brace set left: the rest of the string is literal.
|
|
if (!m) {
|
|
return combine(acc, str, [''], max, maxLength, dropEmpties);
|
|
}
|
|
// no need to expand pre, since it is guaranteed to be free of brace-sets
|
|
const pre = m.pre;
|
|
if (/\$$/.test(pre)) {
|
|
acc = combine(acc, pre + '{' + m.body + '}', [''], max, maxLength, dropEmpties && !m.post.length);
|
|
firstGroup = false;
|
|
if (!m.post.length)
|
|
break;
|
|
str = m.post;
|
|
continue;
|
|
}
|
|
const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
|
|
const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
|
|
const isSequence = isNumericSequence || isAlphaSequence;
|
|
const isOptions = m.body.indexOf(',') >= 0;
|
|
if (!isSequence && !isOptions) {
|
|
// {a},b}
|
|
if (m.post.match(/,(?!,).*\}/)) {
|
|
str = m.pre + '{' + m.body + escClose + m.post;
|
|
isTop = true;
|
|
continue;
|
|
}
|
|
// Nothing here expands, so the whole remaining string is literal.
|
|
return combine(acc, pre + '{' + m.body + '}' + m.post, [''], max, maxLength, dropEmpties);
|
|
}
|
|
if (firstGroup) {
|
|
dropEmpties = isTop && !isSequence;
|
|
firstGroup = false;
|
|
}
|
|
let values;
|
|
if (isSequence) {
|
|
values = expandSequence(m.body, isAlphaSequence, max);
|
|
}
|
|
else {
|
|
let n = parseCommaParts(m.body);
|
|
if (n.length === 1 && n[0] !== undefined) {
|
|
// x{{a,b}}y ==> x{a}y x{b}y
|
|
n = expand_(n[0], max, maxLength, false).map(embrace);
|
|
//XXX is this necessary? Can't seem to hit it in tests.
|
|
/* c8 ignore start */
|
|
if (n.length === 1) {
|
|
acc = combine(acc, pre + n[0], [''], max, maxLength, dropEmpties && !m.post.length);
|
|
if (!m.post.length)
|
|
break;
|
|
str = m.post;
|
|
continue;
|
|
}
|
|
/* c8 ignore stop */
|
|
}
|
|
values = [];
|
|
for (let j = 0; j < n.length; j++) {
|
|
values.push.apply(values, expand_(n[j], max, maxLength, false));
|
|
}
|
|
}
|
|
acc = combine(acc, pre, values, max, maxLength, dropEmpties && !m.post.length);
|
|
if (!m.post.length)
|
|
break;
|
|
str = m.post;
|
|
}
|
|
return acc;
|
|
}
|
|
//# sourceMappingURL=index.js.map
|