UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

503 lines • 20.4 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 { specOf } from "../../spec/builtins.js"; import { FlagView } from "../../spec/flag_view.js"; import { operandStat } from "../utils/operands.js"; import { IOResult } from "../../../io/types.js"; import { CapacityState, DEVICE_NUMBERS_KEY, FileType, Follow, LINK_TARGET_KEY, LineFacts, } from "../../../types.js"; import { isoTimestamp, isoToEpoch } from "../../../utils/dates.js"; import { fsErrorLine } from "../../../errors/render.js"; import { fsStrerror, isFsError } from "../../../errors/fs.js"; import { DQ_BLOCKERS, LEADING_SPECIAL, NAMED_ESCAPES, shellQuoteAlways, } from "../../../utils/quote.js"; import { BLOCK_UNIT, ALL_MODE_BITS, contentSize, deviceRdev, posixMode, sizeUnknown, } from "../../../utils/stat_view.js"; import { fullIsoTime, lsModeString } from "../utils/formatting.js"; import { groupName, identityOf, ownerName } from "../utils/identity.js"; import { formatRecords } from "../utils/output.js"; import { missingOperandError } from "../../spec/usage.js"; import { encodeText } from "../../../shell/bytes.js"; import { linksOf, mountsOf, dispatchOf } from "../utils/links.js"; import { vfsStatPath } from "../utils/paths.js"; const TYPE_LABELS = { [FileType.DIRECTORY]: 'directory', [FileType.SYMLINK]: 'symbolic link', [FileType.CHAR_DEVICE]: 'character special file', [FileType.FILE]: 'regular file', [FileType.FIFO]: 'fifo', }; function typeLabel(s) { return TYPE_LABELS[s.type] ?? 'regular file'; } function effectiveMode(s) { return posixMode(s) & ALL_MODE_BITS; } function epoch(iso) { if (iso === null || iso === '') return '0'; const secs = isoToEpoch(iso); return Number.isNaN(secs) ? '0' : String(secs); } const STR_DIRECTIVES = new Set(['n', 'N', 'F']); const FORMAT_FLAGS = new Set(["'", '-', '+', ' ', '#', '0', 'I']); const ASCII_DIGITS = new Set('0123456789'); // The directives GNU knows (coreutils 9.7). It prints any other as a bare // '?', which no width pads; one it knows that a VFS cannot answer is padded // like the value it stands for. const KNOWN = new Set([...Array.from('aAbBCdDfFgGhimnNorRstTuUwWxXyYzZ%'), 'Hd', 'Ld', 'Hr', 'Lr']); // The placeholders for a value that is not known, which a 0 flag pads with // spaces rather than zeros. const PLACEHOLDERS = new Set(['-', '?']); // The conversions %H and %L split into a device's major and minor; in // file-system mode they are no prefix at all. const DEVICE_HALVES = 'dr'; // GNU's file-system report (coreutils 9.7 stat -f), in its directives. const FS_LAYOUT = [ ' File: "%n"', ' ID: %-8i Namelen: %-7l Type: %T', 'Block size: %-10s Fundamental block size: %S', 'Blocks: Total: %-10b Free: %-10f Available: %a', 'Inodes: Total: %-10c Free: %d', ].join('\n'); // A mount reports its capacity in bytes; -f counts it in 1K blocks, the // unit df reports in. const FS_BLOCK = 1024; const FS_STRINGS = new Set(['n', 'T']); const FS_KNOWN = new Set('abcdfilnsStT%'); const FS_COUNTS = new Set('abcdf'); const FS_STDIN = "using '-' to denote standard input does not work in file system mode"; // What -f reports outside a workspace: nothing is known about the file // system that holds a path. const NO_FILE_SYSTEM = ['-', { state: CapacityState.UNKNOWN }]; // Whether GNU spells a character as a $'..' escape: a control character, or // any byte past ASCII, as the C locale prints it. function needsEscape(char) { return char < ' ' || char >= '\x7f'; } // Spell one character the way bash's $'..' does, a character past ASCII as // the octal escape of each of its UTF-8 bytes. function escapeChar(char) { const named = NAMED_ESCAPES.get(char); if (named !== undefined) return `\\${named}`; return Array.from(encodeText(char), (byte) => '\\' + byte.toString(8).padStart(3, '0')).join(''); } // Whether a name holding an apostrophe still fits in double quotes. GNU only // reaches for them when nothing else in the name would stay live inside them, // so a'b renders as "a'b" but a'b$c does not. # and ~ count as special only // away from the front. function doubleQuotable(name) { for (let index = 0; index < name.length; index += 1) { const char = name.charAt(index); if (needsEscape(char)) return false; if (DQ_BLOCKERS.has(char) && !(index === 0 && LEADING_SPECIAL.has(char))) return false; } return true; } // Single-quoted runs spliced with $'..' escape segments. function singleQuoted(name) { const parts = []; let index = 0; while (index < name.length) { const escaped = needsEscape(name.charAt(index)); let end = index; while (end < name.length && needsEscape(name.charAt(end)) === escaped) end += 1; if (escaped) { // A leading escape keeps the empty quotes GNU emits; a trailing one does not. if (index === 0) parts.push("''"); let text = ''; for (const char of name.slice(index, end)) text += escapeChar(char); parts.push("$'" + text + "'"); } else { parts.push("'" + name.slice(index, end).replaceAll("'", "'\\''") + "'"); } index = end; } return parts.length > 0 ? parts.join('') : "''"; } // Shell-safe quoting for %N, mirroring GNU's default: single quotes are the // rule, with each apostrophe escaped as '\'' and every unprintable character // lifted into a $'..' segment. A name whose only awkward character is an // apostrophe reads better in double quotes, and GNU renders that one case // that way. function quoteName(name) { if (name.includes("'") && doubleQuotable(name)) return `"${name}"`; return singleQuoted(name); } // Apply GNU printf flags, width and precision to a rendered directive; a // precision cuts short only a directive that prints a string (`text`). function applyFlags(value, flags, width, precision, text) { if (precision !== undefined && text) { value = precision === '' ? '' : value.slice(0, Number(precision)); } if (width !== '' && value.length < Number(width)) { const w = Number(width); if (flags.includes('-')) value = value.padEnd(w); else if (flags.includes('0') && !PLACEHOLDERS.has(value)) value = value.padStart(w, '0'); else value = value.padStart(w); } return value; } function directiveValue(spec, s, name, identity) { if (spec === '%') return '%'; if (spec === 'n') return name; if (spec === 's') return sizeUnknown(s) ? '-' : String(contentSize(s)); if (spec === 'F') return typeLabel(s); if (spec === 'a') return effectiveMode(s).toString(8); if (spec === 'A') return lsModeString(s); if (spec === 'f') return posixMode(s).toString(16); if (spec === 'u' || spec === 'U') return ownerName(s.uid, identity); if (spec === 'g' || spec === 'G') return groupName(s.gid, identity); if (spec === 'x') return statTime(s.atime ?? s.modified); if (spec === 'X') return epoch(s.atime ?? s.modified); if (spec === 'y') return statTime(s.modified); if (spec === 'Y') return epoch(s.modified); if (spec === 'z') return statTime(s.ctime); if (spec === 'Z') return epoch(s.ctime); if (spec === 'w') return statTime(s.birthtime); if (spec === 'W') return epoch(s.birthtime); if (spec === 'B') return String(BLOCK_UNIT); if (spec === 'd') return s.dev === null ? '?' : String(s.dev); if (spec === 'D') return s.dev === null ? '?' : s.dev.toString(16); const device = s.extra[DEVICE_NUMBERS_KEY]; const numbers = Array.isArray(device) && device.length === 2 ? device : null; if (spec === 't') return numbers !== null ? numbers[0].toString(16) : '0'; if (spec === 'T') return numbers !== null ? numbers[1].toString(16) : '0'; if (spec === 'r' || spec === 'R') { const rdev = deviceRdev(s); return spec === 'r' ? String(rdev) : rdev.toString(16); } if (spec.length === 2 && (spec.startsWith('H') || spec.startsWith('L'))) { // %Hr/%Lr are rdev major/minor in decimal; %Hd/%Ld are the device the // file resides on: its mount's. const half = spec.startsWith('H') ? 0 : 1; if (spec[1] === 'r' || spec[1] === 'R') { if (numbers === null) return '0'; return String(numbers[half]); } return s.dev === null ? '?' : String(deviceHalves(s.dev)[half]); } return '?'; } /** * A device number's major and minor, split as glibc's `major(3)` and * `minor(3)` split a `dev_t`. The id is up to 48 bits, past the 32 a * bitwise operator keeps, so the shifts are divisions. */ function deviceHalves(dev) { const major = ((Math.floor(dev / 2 ** 8) & 0xfff) | (Math.floor(dev / 2 ** 32) & 0xfffff000)) >>> 0; const minor = ((dev % 2 ** 8) | (Math.floor(dev / 2 ** 12) & 0xffffff00)) >>> 0; return [major, minor]; } // The fields %N renders: the name, plus a symlink's target. GNU shell-quotes // each one only for a bare %N; any flag, width or precision drops the quotes. function nameParts(s, name, quoted) { const parts = [name]; if (s.type === FileType.SYMLINK) { const target = s.extra[LINK_TARGET_KEY]; if (typeof target === 'string' && target !== '') parts.push(target); } return quoted ? parts.map(quoteName) : parts; } function renderDirective(d, s, name, identity) { if (!KNOWN.has(d.spec)) return '?'; if (d.spec === 'N') { // GNU formats the name and a symlink's target as two separate fields, // so a width pads each one rather than the joined line. const bare = d.flags === '' && d.width === '' && d.precision === undefined; return nameParts(s, name, bare) .map((part) => applyFlags(part, d.flags, d.width, d.precision, true)) .join(' -> '); } let value = directiveValue(d.spec, s, name, identity); if (d.flags.includes('#') && d.spec === 'a' && !value.startsWith('0')) value = '0' + value; return applyFlags(value, d.flags, d.width, d.precision, STR_DIRECTIVES.has(d.spec)); } // A byte count as 1K blocks, rounded up like df, or '-' when unknown. function fsBlocks(nbytes) { return nbytes === null || nbytes === undefined ? '-' : String(Math.ceil(nbytes / FS_BLOCK)); } // One file-system directive's value. A mount has no file system ID, name // limit, type number or transfer size, so those print '?'. Its counts print // '-' unless it reports a quota, as df shows them. function fsValue(spec, kind, cap, name) { if (spec === '%') return '%'; if (spec === 'n') return name; if (spec === 'T') return kind; if (spec === 'S') return String(FS_BLOCK); if (!FS_COUNTS.has(spec)) return '?'; if (cap.state !== CapacityState.QUOTA) return '-'; if (spec === 'b') return fsBlocks(cap.total); if (spec === 'a') return fsBlocks(cap.available); if (spec === 'f') { if (cap.total == null || cap.used == null) return '-'; return fsBlocks(cap.total - cap.used); } if (spec === 'c') return cap.inodes == null ? '-' : String(cap.inodes); if (cap.inodes == null || cap.inodesUsed == null) return '-'; return String(cap.inodes - cap.inodesUsed); } function renderFsDirective(d, kind, cap, name) { if (!FS_KNOWN.has(d.spec)) return '?'; return applyFlags(fsValue(d.spec, kind, cap, name), d.flags, d.width, d.precision, FS_STRINGS.has(d.spec)); } // Scan one directive starting at a `%`. Any character converts, and one GNU // does not know prints '?'; `halves` are the conversions an H or L prefix // takes. function parseFormatDirective(fmt, start, halves) { let cursor = start + 1; let flags = ''; while (cursor < fmt.length && FORMAT_FLAGS.has(fmt[cursor] ?? '')) { flags += fmt.charAt(cursor); cursor += 1; } let width = ''; while (cursor < fmt.length && ASCII_DIGITS.has(fmt[cursor] ?? '')) { width += fmt.charAt(cursor); cursor += 1; } let precision; if (fmt[cursor] === '.') { cursor += 1; precision = ''; while (cursor < fmt.length && ASCII_DIGITS.has(fmt[cursor] ?? '')) { precision += fmt.charAt(cursor); cursor += 1; } } if (cursor >= fmt.length) return null; const first = fmt.charAt(cursor); let spec = first; cursor += 1; if ((first === 'H' || first === 'L') && cursor < fmt.length && halves.includes(fmt.charAt(cursor))) { spec += fmt.charAt(cursor); cursor += 1; } return { end: cursor, flags, width, precision, spec }; } // Expand a format string, each directive through `render`. function format(fmt, halves, render) { const parts = []; let cursor = 0; while (cursor < fmt.length) { const start = fmt.indexOf('%', cursor); if (start === -1) { parts.push(fmt.slice(cursor)); break; } parts.push(fmt.slice(cursor, start)); const directive = parseFormatDirective(fmt, start, halves); if (directive === null) { parts.push('%'); cursor = start + 1; continue; } parts.push(render(directive)); cursor = directive.end; } return parts.join(''); } function formatStat(fmt, s, name, identity) { return format(fmt, DEVICE_HALVES, (d) => renderDirective(d, s, name, identity)); } // The fraction of a second as the backend spelled it, so both hosts print // the digits the stamp carries rather than what their clock type keeps. /** A known timestamp in GNU's layout, in UTC, or '-' when unknown. A naive * stamp is UTC, as everywhere else a backend time is read. */ function statTime(value) { return isoTimestamp(value) === null ? '-' : fullIsoTime(value); } /** GNU coreutils 9.7's default layout, with unknown fields marked. A VFS has * rendered bytes, modes, logical owners and its mount's device, but no inode, * allocation blocks, IO block size or link count: those print '?'. An absent size, * owner number or time prints '-'. Each time is the one its directive * prints (`%x %y %z %w`), the name is unquoted as GNU's default prints it, * and times are UTC. */ function renderStat(s, name, identity) { const size = directiveValue('s', s, name, identity); const uid = String(s.uid ?? '-').padStart(5); const gid = String(s.gid ?? '-').padStart(5); const owner = ownerName(s.uid, identity).padStart(8); const group = groupName(s.gid, identity).padStart(8); const links = s.type === FileType.CHAR_DEVICE ? `Links: ${'?'.padEnd(5)} Device type: ${directiveValue('Hr', s, name, identity)},${directiveValue('Lr', s, name, identity)}` : 'Links: ?'; const device = s.dev === null ? '?' : deviceHalves(s.dev).join(','); return [ ` File: ${nameParts(s, name, false).join(' -> ')}`, ` Size: ${size.padEnd(10)}\tBlocks: ${'?'.padEnd(10)} IO Block: ${'?'.padEnd(6)} ${typeLabel(s)}`, `Device: ${device}\tInode: ${'?'.padEnd(10)} ${links}`, `Access: (${effectiveMode(s).toString(8).padStart(4, '0')}/${lsModeString(s)}) Uid: (${uid}/${owner}) Gid: (${gid}/${group})`, `Access: ${statTime(s.atime ?? s.modified)}`, `Modify: ${statTime(s.modified)}`, `Change: ${statTime(s.ctime)}`, ` Birth: ${statTime(s.birthtime)}`, ].join('\n'); } // One operand's report: the -c format when given, else GNU's default layout. function renderOne(s, name, fmt, identity) { if (fmt !== null) return formatStat(fmt, s, name, identity); return renderStat(s, name, identity); } // Report the file system each operand is on, GNU `stat -f`. The operand stat // settles that a path exists first, so a missing one fails in the words a // plain stat would find for it; `statfs` is null outside a workspace. async function fileSystems(paths, fmt, probe, statfs) { const lines = []; let err = ''; for (const p of paths) { if (p.rawPath === '-') { err += `stat: ${FS_STDIN}\n`; continue; } let found; try { await probe(p); found = statfs !== null ? await statfs(p) : NO_FILE_SYSTEM; } catch (e) { if (!isFsError(e)) throw e; const strerror = fsStrerror(e); err += `stat: cannot read file system information for ${shellQuoteAlways(p.rawPath)}` + `${strerror !== null ? `: ${strerror}` : ''}\n`; continue; } const [kind, cap] = found; lines.push(format(fmt, '', (d) => renderFsDirective(d, kind, cap, p.rawPath))); } const io = new IOResult({ exitCode: err === '' ? 0 : 1, stderr: err === '' ? null : encodeText(err), }); if (lines.length === 0) return [null, io]; return [formatRecords(lines), io]; } // statfs through the dispatcher. async function dispatchedStatfs(dispatch, path) { const [result] = (await dispatch('statfs', path)); return result; } function parseFlags(bag) { const fl = new FlagView(bag, specOf('stat')); return { format: fl.asStr('format') ?? null, fileSystem: fl.asBool('file_system'), deref: fl.asBool('dereference'), }; } export async function statGeneric(paths, ctx, stat) { const parsed = parseFlags(ctx.flags); if (paths.length === 0) throw missingOperandError('stat', null); const fmt = parsed.format; const statPath = vfsStatPath(ctx.vfs); if (parsed.fileSystem) { const dispatch = dispatchOf(ctx); const probe = (p) => operandStat(p, stat, statPath, mountsOf(ctx), linksOf(ctx)); const statfs = dispatch !== undefined ? (p) => dispatchedStatfs(dispatch, p) : null; return fileSystems(paths, fmt ?? FS_LAYOUT, probe, statfs); } const lines = []; let err = ''; const links = parsed.deref ? null : (linksOf(ctx) ?? null); const identity = identityOf(ctx); for (const p of paths) { // GNU stat lstats: a symlink operand reports the link itself, not // its target, unless -L asks to dereference. A link has no backend // inode, so the namespace is the only authority for it. const linked = links?.statAt(p.virtual) ?? null; if (linked !== null) { lines.push(renderOne(linked, p.rawPath, fmt, identity)); continue; } let s; try { s = await operandStat(p, stat, statPath, mountsOf(ctx), linksOf(ctx)); } catch (e) { // GNU stat keeps reporting the remaining operands, exit 1. if (!isFsError(e)) throw e; err += fsErrorLine('stat', p, e); continue; } lines.push(renderOne(s, p.rawPath, fmt, identity)); } const io = new IOResult({ exitCode: err === '' ? 0 : 1, stderr: err === '' ? null : encodeText(err), }); if (lines.length === 0) return [null, io]; const out = formatRecords(lines); return [out, io]; } /** A stat line reports a link itself unless `-L`. Mirrors Python's `facts`. */ export function facts(fl, _operands) { return new LineFacts({ follows: fl.asBool('dereference') ? Follow.LAST : Follow.NONE }); } //# sourceMappingURL=stat.js.map