@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
249 lines • 9.65 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 { concat } from "../utils/bytes.js";
const ENC = new TextEncoder();
const DECODER = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
const SURROGATE_BASE = 0xdc00;
const SURROGATE_LOW = 0xdc80;
const SURROGATE_HIGH = 0xdcff;
const HIGH_SURROGATE_LOW = 0xd800;
const HIGH_SURROGATE_HIGH = 0xdbff;
const ASCII_MAX = 0x80;
const BYTE_MASK = 0xff;
const VIEW_CHUNK = 8192;
const LOCALE_VARS = ['LC_ALL', 'LC_CTYPE', 'LANG'];
const UTF8_CODESET = 'utf8';
/**
* Stand in for one raw output byte inside a text string.
*
* `\xHH` and `\NNN` name a byte, not a code point: bash writes `\xff` as
* the single byte 0xFF, which is not valid UTF-8 on its own and so has no
* character to stand for it. A byte above ASCII is therefore carried as
* its surrogate escape, the same convention Python's own filesystem paths
* use, and `encodeText` turns it back into that byte.
*
* Three octal digits reach past one byte (`\400` is 256, `\777` is 511)
* and bash writes the low byte of those, so the value is masked rather
* than refused.
*/
export function byteChar(value) {
const byte = value & BYTE_MASK;
return String.fromCharCode(byte < ASCII_MAX ? byte : SURROGATE_BASE + byte);
}
/**
* Whether the code unit at `i` is a byte this module smuggled in.
*
* The sentinels are lone low surrogates, and a low surrogate that follows
* a high one is half of an ordinary non-BMP character (`𐂀` is
* U+D800 U+DC80) that must be encoded as itself.
*/
function isByteSentinel(text, i) {
const code = text.charCodeAt(i);
if (code < SURROGATE_LOW || code > SURROGATE_HIGH)
return false;
if (i === 0)
return true;
const before = text.charCodeAt(i - 1);
return before < HIGH_SURROGATE_LOW || before > HIGH_SURROGATE_HIGH;
}
/**
* Encode shell text for output, byte escapes included.
*
* Every place the shell turns its own text into bytes goes through here,
* because a string that reached it from `byteChar` holds a lone surrogate
* that `TextEncoder` would write as U+FFFD.
*/
export function encodeText(text) {
let first = -1;
for (let i = 0; i < text.length; i++) {
if (isByteSentinel(text, i)) {
first = i;
break;
}
}
if (first === -1)
return ENC.encode(text);
const parts = [];
let run = '';
for (let i = 0; i < text.length; i++) {
if (isByteSentinel(text, i)) {
if (run !== '') {
parts.push(ENC.encode(run));
run = '';
}
parts.push(new Uint8Array([text.charCodeAt(i) - SURROGATE_BASE]));
}
else {
run += text.charAt(i);
}
}
if (run !== '')
parts.push(ENC.encode(run));
return concat(parts);
}
/**
* Read bytes back as shell text, the inverse of `encodeText`.
*
* Valid UTF-8 comes back as its characters and every other byte as its
* surrogate escape, the stand-in `byteChar` makes, so the text round-trips
* to exactly the bytes it was read from. `TextDecoder`'s replacement cannot:
* one invalid byte becomes U+FFFD, three bytes wide, and every byte offset
* counted back past it runs ahead of GNU's.
*/
export function decodeText(raw) {
try {
return DECODER.decode(raw);
}
catch (error) {
if (!(error instanceof TypeError))
throw error;
}
const parts = [];
const units = new Uint16Array(Math.min(raw.length, 8192));
let used = 0;
for (let i = 0; i < raw.length;) {
const byte = raw[i] ?? 0;
const second = raw[i + 1] ?? 0;
const third = raw[i + 2] ?? 0;
const fourth = raw[i + 3] ?? 0;
let code = byte < ASCII_MAX ? byte : SURROGATE_BASE + byte;
let width = 1;
// Reject overlong encodings, surrogate code points and values above
// U+10FFFF. An invalid sequence escapes only its first byte, just as
// Python's surrogateescape does, then retries at the following byte.
if (byte >= 0xc2 && byte <= 0xdf && second >= 0x80 && second <= 0xbf) {
code = ((byte & 0x1f) << 6) | (second & 0x3f);
width = 2;
}
else if (byte >= 0xe0 &&
byte <= 0xef &&
second >= (byte === 0xe0 ? 0xa0 : 0x80) &&
second <= (byte === 0xed ? 0x9f : 0xbf) &&
third >= 0x80 &&
third <= 0xbf) {
code = ((byte & 0x0f) << 12) | ((second & 0x3f) << 6) | (third & 0x3f);
width = 3;
}
else if (byte >= 0xf0 &&
byte <= 0xf4 &&
second >= (byte === 0xf0 ? 0x90 : 0x80) &&
second <= (byte === 0xf4 ? 0x8f : 0xbf) &&
third >= 0x80 &&
third <= 0xbf &&
fourth >= 0x80 &&
fourth <= 0xbf) {
code = ((byte & 7) << 18) | ((second & 0x3f) << 12) | ((third & 0x3f) << 6) | (fourth & 0x3f);
width = 4;
}
if (code > 0xffff) {
code -= 0x10000;
units[used++] = 0xd800 + (code >> 10);
units[used++] = 0xdc00 + (code & 0x3ff);
}
else {
units[used++] = code;
}
i += width;
if (used >= units.length - 1) {
parts.push(String.fromCharCode(...units.subarray(0, used)));
used = 0;
}
}
if (used > 0)
parts.push(String.fromCharCode(...units.subarray(0, used)));
return parts.join('');
}
/**
* Text whose raw bytes read as the characters they spell together.
*
* A word joined from pieces can hold byte stand-ins that only spell a
* character side by side: `$'\xc3'$'\xa9'` is the two bytes of `é`, each
* escaped alone. bash holds the word as bytes, so it is `é`, the same word
* `$'\xc3\xa9'` gives, to compare, measure and name a file by. Mirrors
* Python's `join_bytes`.
*/
export function joinBytes(text) {
for (let i = 0; i < text.length; i++) {
if (isByteSentinel(text, i))
return decodeText(encodeText(text));
}
return text;
}
/**
* Whether the environment names a UTF-8 locale, as setlocale reads it.
*
* POSIX takes the character type from the first of `LC_ALL`, `LC_CTYPE` and
* `LANG` that is set and not empty, so `LC_ALL=C` outranks `LANG=C.UTF-8`. A
* name is `language[_territory][.codeset][@modifier]`, and glibc compares the
* codeset with case and punctuation dropped, so `C.UTF-8`, `en_US.utf8` and
* `de_DE.UTF-8@euro` all name UTF-8. Every UTF-8 name counts as installed,
* where glibc falls back to the C locale for one it lacks. Mirrors Python's
* `utf8_locale`.
*/
export function utf8Locale(env) {
for (const name of LOCALE_VARS) {
const value = env?.[name] ?? '';
if (value !== '') {
const codeset = value.split('@')[0]?.split('.').slice(1).join('.') ?? '';
return codeset.toLowerCase().replace(/[^a-z0-9]/g, '') === UTF8_CODESET;
}
}
return false;
}
/**
* The same bytes as a string of one character per byte.
*
* A command that runs in GNU's C locale (grep, sed, awk, tr, expr) counts,
* matches and indexes bytes, not characters: `.` matches one byte, `length`
* counts bytes, and a match may end inside a character. All of that follows
* from running on this representation and converting only at the command's
* edges, where a typed `é`, its `$'\xc3\xa9'` spelling and the file's own
* bytes all arrive as the same two characters. Mirrors Python's `byte_view`.
*
* Under a UTF-8 locale (`utf8`) grep, sed and expr count, match and index
* characters instead, so the view is the text itself: a byte that is no part
* of a character stays its surrogate escape, one element of its own, as it
* does in glibc's matcher.
*/
export function byteView(value, utf8 = false) {
if (utf8)
return typeof value === 'string' ? value : decodeText(value);
const bytes = typeof value === 'string' ? encodeText(value) : value;
const parts = [];
for (let at = 0; at < bytes.length; at += VIEW_CHUNK)
parts.push(String.fromCharCode(...bytes.subarray(at, at + VIEW_CHUNK)));
return parts.join('');
}
/**
* The bytes a byte view stands for, the inverse of `byteView`. An invalid
* sequence or half a character comes back as itself, which is what GNU
* writes. Under `utf8` the view is the text itself.
*/
export function fromByteView(view, utf8 = false) {
if (utf8)
return encodeText(view);
const out = new Uint8Array(view.length);
for (let at = 0; at < view.length; at++) {
const byte = view.charCodeAt(at);
if (byte > BYTE_MASK)
throw new RangeError('byte view contains a non-byte character');
out[at] = byte;
}
return out;
}
/** A byte view as shell text again, for a path or a nested command line. */
export function textView(view, utf8 = false) {
return utf8 ? view : decodeText(fromByteView(view));
}
//# sourceMappingURL=bytes.js.map