UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

248 lines • 8.87 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { CHAR_SEQ, INERT_CLOSE, INERT_OPEN, NUM_SEQ } from "./constants.js"; import { piece } from "./types.js"; // Encode an already-expanded chunk as an opaque template atom. Inert // atoms never contribute brace metacharacters, matching bash's ordering // where brace expansion runs before parameter and command substitution // (`{a,$v}` alternates on the atom, `{1..$n}` stays literal). Shell // input cannot contain NUL, so the sentinels cannot collide. export function makeInert(index) { return `${INERT_OPEN}${String(index)}${INERT_CLOSE}`; } // Replace inert atoms in an expanded template word with their pieces. The // template's own text is literal and never splits; an atom keeps the // pieces its expansion produced. An empty word has no pieces, so it is no // word at all: `{,x}` is `x`. export function substitute(word, values) { const out = []; let i = 0; for (;;) { const j = word.indexOf(INERT_OPEN, i); const literal = j < 0 ? word.slice(i) : word.slice(i, j); if (literal !== '') out.push(piece(literal)); if (j < 0) return out; const k = word.indexOf(INERT_CLOSE, j); for (const c of values[Number(word.slice(j + 1, k))] ?? []) out.push(c); i = k + 1; } } function isPadded(text) { const digits = text.startsWith('-') ? text.slice(1) : text; return digits.length > 1 && digits.startsWith('0'); } function parseInteger(text) { const negative = text.startsWith('-'); const digits = (negative || text.startsWith('+') ? text.slice(1) : text).replace(/^0+/, '') || '0'; if (digits.length > 19) return null; const value = BigInt(digits) * (negative ? -1n : 1n); return value >= -(1n << 63n) && value < 1n << 63n ? value : null; } function parseStep(stepText, descending) { if (stepText === undefined) return 1n; const value = parseInteger(stepText); if (value === null || (value === -(1n << 63n) && !descending)) return null; const step = value < 0n ? -value : value; return step === 0n ? 1n : step; } function* seqValues(lo, hi, step, word) { if (lo <= hi) { for (let v = lo; v <= hi; v += step) yield word(v); } else { for (let v = lo; v >= hi; v -= step) yield word(v); } } function padValue(v, width) { const sign = v < 0n ? '-' : ''; return sign + (v < 0n ? -v : v).toString().padStart(width - sign.length, '0'); } // Generate `{x..y[..step]}` sequence words, or null if not a range. A // range body must be pure literal text: an inert atom or escape anywhere // inside disqualifies it (`{1..$n}` stays literal in bash). Numeric // endpoints with leading zeros zero-pad every output to the widest // endpoint, the minus sign counting toward the width ({-05..5..5} yields // -05 000 005). Step direction follows the endpoints; the step's own // sign is ignored and 0 acts as 1. The words are made as they are read. function genSequence(body) { if (body.includes(INERT_OPEN) || body.includes('\\')) return null; const num = NUM_SEQ.exec(body); if (num !== null && num[0] === body) { const [, loText = '', hiText = '', stepText] = num; const lo = parseInteger(loText); const hi = parseInteger(hiText); if (lo === null || hi === null) return null; const step = parseStep(stepText, lo >= hi); if (step === null) return null; // Pinned Bash leaves these directed spans literal, even with a large step. const maxSpan = (1n << 63n) - 3n; if ((lo < 0n && hi - lo > maxSpan) || (lo > 0n && lo - hi > maxSpan)) return null; if (isPadded(loText) || isPadded(hiText)) { const width = Math.max(loText.length, hiText.length); return seqValues(lo, hi, step, (v) => padValue(v, width)); } return seqValues(lo, hi, step, (v) => String(v)); } const chr = CHAR_SEQ.exec(body); if (chr !== null && chr[0] === body) { const [, loText = '', hiText = '', stepText] = chr; const step = parseStep(stepText, loText >= hiText); if (step === null) return null; return seqValues(BigInt(loText.charCodeAt(0)), BigInt(hiText.charCodeAt(0)), step, (v) => String.fromCharCode(Number(v))); } return null; } function matchClose(text, openIdx) { let depth = 1; let i = openIdx + 1; const n = text.length; while (i < n) { const ch = text[i]; if (ch === '\\') { i += 2; continue; } if (ch === INERT_OPEN) { i = text.indexOf(INERT_CLOSE, i) + 1; continue; } if (ch === '{') depth += 1; else if (ch === '}') { depth -= 1; if (depth === 0) return i; } i += 1; } return -1; } function splitAlternatives(body) { const alts = []; let depth = 0; let start = 0; let i = 0; const n = body.length; while (i < n) { const ch = body[i]; if (ch === '\\') { i += 2; continue; } if (ch === INERT_OPEN) { i = body.indexOf(INERT_CLOSE, i) + 1; continue; } if (ch === '{') depth += 1; else if (ch === '}') depth -= 1; else if (ch === ',' && depth === 0) { alts.push(body.slice(start, i)); start = i + 1; } i += 1; } if (alts.length === 0) return null; alts.push(body.slice(start)); return alts; } function* expand(template) { let i = 0; const n = template.length; while (i < n) { const ch = template[i]; if (ch === '\\') { i += 2; continue; } if (ch === INERT_OPEN) { i = template.indexOf(INERT_CLOSE, i) + 1; continue; } if (ch !== '{') { i += 1; continue; } const close = matchClose(template, i); if (close < 0) { i += 1; continue; } const body = template.slice(i + 1, close); const sequence = genSequence(body); let groups; if (sequence === null) { const alts = splitAlternatives(body); if (alts === null) { // `{abc}` and friends stay literal; the next `{` (even one // inside this body) may still expand, like GNU. i += 1; continue; } groups = alts.map((alt) => expand(alt)); } else { groups = [sequence]; } const prefix = template.slice(0, i); // The suffix words are made once, as the first alternative takes them, // and kept for the rest. const suffixes = []; let tail = expand(template.slice(close + 1)); for (const group of groups) { for (const alt of group) { if (tail !== null) { for (const suffix of tail) { suffixes.push(suffix); yield prefix + alt + suffix; } tail = null; continue; } for (const suffix of suffixes) yield prefix + alt + suffix; } } return; } yield template; } // Brace-expand a template word into its word list. Returns the expanded // words (inert atoms preserved), or null when the template contains // nothing brace-expandable so callers can fall back to plain // concatenation. The words are made as they are read, so a caller that // yields as it reads them stops inside a long range. A word an expansion // makes is shorter than the template it came from, so a first word equal // to the template means nothing expanded. export function expandTemplate(template) { if (expand(template).next().value === template) return null; return expand(template); } //# sourceMappingURL=brace.js.map