@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
248 lines • 8.87 kB
JavaScript
// ========= 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