@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
237 lines • 8.23 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 { shlexSplit } from "../utils/shlex.js";
import { byteChar, decodeText, encodeText } from "./bytes.js";
// The ANSI-C escape table bash decodes $'...' with. \e/\E
// are here although printf lacks them; \c takes an argument here while
// printf's \c means stop, which is why the printf reader is not reused.
const SIMPLE = {
a: '\x07',
b: '\b',
e: '\x1b',
E: '\x1b',
f: '\f',
n: '\n',
r: '\r',
t: '\t',
v: '\v',
'\\': '\\',
"'": "'",
'"': '"',
'?': '?',
};
const HEX = /[0-9a-fA-F]/;
const OCTAL = /[0-7]/;
function scanHex(content, start, limit) {
let end = start;
while (end < content.length && end - start < limit && HEX.test(content[end] ?? '')) {
end += 1;
}
return content.slice(start, end);
}
/**
* bash's UTF-8 encoding of an escape's value past ASCII.
*
* UTF-8-shaped bytes for any 32-bit value: surrogate halves encode like
* ordinary three-byte characters, values past Unicode take the old-style
* four- to six-byte forms, and 0x80000000 and past produce nothing at all.
*/
function u32Utf8(value) {
if (value < 0x800)
return [0xc0 | (value >> 6), 0x80 | (value & 0x3f)];
if (value < 0x10000) {
return [0xe0 | (value >> 12), 0x80 | ((value >> 6) & 0x3f), 0x80 | (value & 0x3f)];
}
if (value < 0x200000) {
return [
0xf0 | (value >> 18),
0x80 | ((value >> 12) & 0x3f),
0x80 | ((value >> 6) & 0x3f),
0x80 | (value & 0x3f),
];
}
if (value < 0x4000000) {
return [
0xf8 | (value >> 24),
0x80 | ((value >> 18) & 0x3f),
0x80 | ((value >> 12) & 0x3f),
0x80 | ((value >> 6) & 0x3f),
0x80 | (value & 0x3f),
];
}
if (value < 0x80000000) {
return [
0xfc | (value >> 30),
0x80 | ((value >> 24) & 0x3f),
0x80 | ((value >> 18) & 0x3f),
0x80 | ((value >> 12) & 0x3f),
0x80 | ((value >> 6) & 0x3f),
0x80 | (value & 0x3f),
];
}
return [];
}
/**
* The text a `\u` or `\U` escape writes for its value.
*
* bash UTF-8-encodes every value under a UTF-8 locale: a
* valid scalar is its character, while surrogate halves and values past
* Unicode become raw UTF-8-shaped bytes, and 0x80000000 and past produce
* nothing. Pinned: `\uD800` is ed a0 80, `\U00110000` is f4 90 80 80,
* `\UFFFFFFFF` is empty.
*/
export function codePointText(value) {
if (value <= 0x7f || (value <= 0x10ffff && !(value >= 0xd800 && value <= 0xdfff))) {
return String.fromCodePoint(value);
}
return u32Utf8(value).map(byteChar).join('');
}
/**
* The decoded pieces as text, a byte run read back as its characters.
*
* bash builds a $'...' word as bytes, so `\xc3\xa9` is the two bytes of an é
* and names the same file as a typed one. The escapes arrive one raw byte at
* a time, so the joined word is read back once: a run that is valid UTF-8
* becomes its characters and any other byte stays raw.
*/
function asText(out) {
return decodeText(encodeText(out.join('')));
}
/**
* Decode the body of a $'...' word to the text it names.
*
* Follows bash 5.2 (under a UTF-8 locale): simple
* escapes, 1-3 octal digits with the value masked to a byte, \xHH
* bytes, \u and \U values UTF-8-encoded (surrogates and
* values past Unicode come out as raw UTF-8-shaped bytes), \cX control
* characters (X of `?` is DEL, an escaped backslash counts as one
* operand), and any other or incomplete escape kept verbatim, backslash
* included. A NUL truncates the rest of this word segment, the C-string
* behavior; the segment alone is cut, so `x$'a\0b'y` still expands to
* `xay`.
*/
export function decodeAnsiC(content) {
const out = [];
let i = 0;
while (i < content.length) {
const char = content[i] ?? '';
if (char !== '\\' || i + 1 === content.length) {
out.push(char);
i += 1;
continue;
}
const marker = content[i + 1] ?? '';
const simple = SIMPLE[marker];
if (simple !== undefined) {
out.push(simple);
i += 2;
continue;
}
if (OCTAL.test(marker)) {
let end = i + 1;
while (end < content.length && end - i <= 3 && OCTAL.test(content[end] ?? '')) {
end += 1;
}
const value = parseInt(content.slice(i + 1, end), 8);
if ((value & 0xff) === 0)
return asText(out);
out.push(byteChar(value));
i = end;
continue;
}
if (marker === 'x') {
const digits = scanHex(content, i + 2, 2);
if (digits === '') {
out.push('\\x');
i += 2;
continue;
}
const value = parseInt(digits, 16);
if (value === 0)
return asText(out);
out.push(byteChar(value));
i += 2 + digits.length;
continue;
}
if (marker === 'u' || marker === 'U') {
const digits = scanHex(content, i + 2, marker === 'u' ? 4 : 8);
if (digits === '') {
out.push(`\\${marker}`);
i += 2;
continue;
}
const value = parseInt(digits, 16);
if (value === 0)
return asText(out);
out.push(codePointText(value));
i += 2 + digits.length;
continue;
}
if (marker === 'c') {
if (i + 2 === content.length) {
out.push('\\c');
i += 2;
continue;
}
const operand = content[i + 2] ?? '';
i += 3;
if (operand === '\\' && content[i] === '\\') {
// \c\\ spells an escaped backslash operand; both characters
// belong to it.
i += 1;
}
const value = operand === '?' ? 0x7f : (operand.toUpperCase().codePointAt(0) ?? 0) & 0x1f;
if (value === 0)
return asText(out);
out.push(String.fromCharCode(value));
continue;
}
out.push(char + marker);
i += 2;
}
return asText(out);
}
// The text an unquoted word's backslash escapes name.
export function unescapeUnquoted(text) {
if (!text.includes('\\'))
return text;
const parts = shlexSplit(text);
return parts[0] ?? text;
}
// The text a double-quoted segment's escapes name. Bash recognizes
// \$, \`, \", \\ and \<newline> inside double quotes; every other
// backslash stays.
export function unescapeDquoted(text) {
const NUL = String.fromCharCode(0);
let out = text;
out = out.replaceAll('\\\\', NUL);
out = out.replaceAll('\\"', '"');
out = out.replaceAll('\\$', '$');
out = out.replaceAll('\\`', '`');
out = out.replaceAll('\\\n', '');
return out.replaceAll(NUL, '\\');
}
/**
* Whether an odd run of backslashes ends `text`, so its last backslash
* escapes what follows: the newline it continues, or the end of the input it
* quotes. An even run is escaped backslashes only. Mirrors Python's
* ends_escaped.
*/
export function endsEscaped(text) {
let run = 0;
while (run < text.length && text.charCodeAt(text.length - 1 - run) === 92)
run += 1;
return run % 2 === 1;
}
//# sourceMappingURL=escapes.js.map