@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
92 lines • 3.93 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 { fromUnits, units } from "../rs/document.js";
import { openStreamSync } from "../rs/stream.js";
// POSIX NAME_MAX on ext4 and APFS alike, and it counts BYTES. Truncating by
// characters is the same number only for ASCII: a 100-character CJK title is
// 300 bytes.
export const NAME_MAX_BYTES = 255;
// What `/` becomes inside a path segment: U+2215 DIVISION SLASH, the one
// character every backend renders a slash as, so a value cannot open a
// directory boundary. `core/hierarchy/codec` is what inverts it.
export const SAFE_SLASH = '∕';
// What marks the next character of a path segment as literal: U+2044
// FRACTION SLASH. `pathSafeName` leads a dot-led name with it, since the
// hierarchy hides a dot-led segment, and `core/hierarchy/codec` spells its
// reversible encoding with it.
export const ESCAPE_LEAD = '⁄';
const ENCODER = new TextEncoder();
const DECODER = new TextDecoder();
/** Answer one of mirage-rs's filename rules, its text as code points. */
function sanitize(config) {
const stream = openStreamSync('sanitize', config);
try {
return JSON.parse(DECODER.decode(stream.finish()));
}
finally {
stream.close();
}
}
/** Whether the text is empty or nothing but Unicode White_Space. */
export function isBlank(text) {
return sanitize({ op: 'blank', text: units(text) }) === true;
}
/** Measure a string the way the filesystem does: in UTF-8 bytes. */
export function byteLength(text) {
return ENCODER.encode(text).length;
}
/** Trim a string to fit a byte budget without splitting a character. */
export function truncateBytes(text, budget) {
return fromUnits(sanitize({ op: 'truncate', text: units(text), budget }));
}
/** Trim trailing underscores, linearly. python's `str.rstrip("_")`. */
export function stripTrailingUnderscores(value) {
let end = value.length;
while (end > 0 && value[end - 1] === '_')
end -= 1;
return value.slice(0, end);
}
/**
* Sanitize a name for use in virtual paths: shell-unsafe characters and
* spaces become underscores, so the name needs no quoting. Never dot-led,
* never empty (`unknown`), at most 100 characters.
*/
export function sanitizeName(name) {
return fromUnits(sanitize({ op: 'name', text: units(name) }));
}
/**
* Make a name safe to embed in a VFS path segment: the original spelling,
* `/` replaced with `SAFE_SLASH` and a dot-led name led with `ESCAPE_LEAD`,
* since the hierarchy hides a dot-led segment. A blank name is `unknown`.
*/
export function pathSafeName(name) {
return fromUnits(sanitize({ op: 'path', text: units(name) }));
}
/**
* Sanitize an API-supplied label for use inside a filename: as
* `sanitizeName`, but ellipsized past either budget, so a truncated name
* reads as truncated. `maxLen` is the readable length a backend wants and
* `maxBytes` what the filesystem accepts; pass the bytes the rest of the
* filename does not already use.
*/
export function sanitizeLabel(text, options) {
return fromUnits(sanitize({
op: 'label',
text: units(text),
fallback: options.fallback,
max_len: options.maxLen,
max_bytes: options.maxBytes ?? NAME_MAX_BYTES,
}));
}
//# sourceMappingURL=sanitize.js.map