OverviewFilesCommitsIssuesDocsReleasesPeers

mirall-cli / node_modules / brace-expansion / dist / commonjs / index.js

263 lines · javascript ref:
"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