UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

403 lines • 14.9 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 { closeQuietly, primeStream } from "../../../io/stream.js"; import { specOf } from "../../spec/builtins.js"; import { FlagView } from "../../spec/flag_view.js"; import {} from "../../spec/types.js"; import { CHUNK_SIZE, chunks } from "../../../io/cooperative.js"; import { ByteSpool } from "../../../io/spool.js"; import { YieldBudget } from "../../../io/yield_budget.js"; import { singleChunk } from "../utils/operands.js"; import { IOResult } from "../../../io/types.js"; import { LineFacts } from "../../../types.js"; import { fsErrorLine } from "../../../errors/render.js"; import { isEnoent, isFsError } from "../../../errors/fs.js"; import { isStdin, resolveSource, stdinStream, streamOutput } from "../utils/stream.js"; import { StreamRefusal } from "../../../rs/errors.js"; import { refused, scan } from "../../../rs/scan.js"; import { openStream, runStream, streamOutputChunks } from "../../../rs/stream.js"; import { asyncChain } from "../../../io/stream.js"; import { extraOperandError, readFailExitCode } from "../../spec/usage.js"; import { CommandName } from "../../spec/types.js"; const ENC = new TextEncoder(); // xxd's exit for an OUTFILE it cannot open, for an INFILE it cannot seek // where -s says, and for a dump that seeks backwards on a stream (vim's xxd // 2024-12-07). const OPEN_OUTPUT_EXIT = 3; const CANNOT_SEEK_EXIT = 4; const SEEK_BACK_EXIT = 5; const CANNOT_SEEK = 'xxd: Sorry, cannot seek.\n'; function dumpStream(source, cols, group, uppercase, base) { return openStream('xxd', { mode: 'dump', cols, group, uppercase, base }).then((stream) => streamOutput(stream, source)); } function plainStream(source, uppercase, cols) { return openStream('xxd', { mode: 'plain', uppercase, cols }).then((stream) => streamOutput(stream, source)); } /** * Revert a hexdump onto a stream, which can only move forward. Deliberate * divergence: stdout is a stream here even when the line redirects it to a * file, which xxd would seek, so an offset before the bytes written stops the * dump with xxd's refusal. Mirrors Python's _xxd_reverse_stream. */ async function* xxdReverseStream(source, io, plain) { const stream = await openStream('xxd', { mode: 'reverse', plain, segments: false }); try { yield* streamOutputChunks(stream, source); } catch (error) { if (!(error instanceof StreamRefusal)) throw error; if (error.output.byteLength > 0) yield error.output; io.exitCode = SEEK_BACK_EXIT; io.stderr = ENC.encode(`${error.message}\n`); } } /** Decode Rust's offset/length frames one bounded write at a time. */ async function* reverseSegments(source) { const header = new Uint8Array(16); const view = new DataView(header.buffer); let held = 0; let left = 0; let start = 0; const pending = new Uint8Array(CHUNK_SIZE); let pendingSize = 0; let pendingStart = 0; for await (const part of chunks(source)) { let at = 0; while (at < part.byteLength) { if (left === 0) { const taken = Math.min(header.byteLength - held, part.byteLength - at); header.set(part.subarray(at, at + taken), held); held += taken; at += taken; if (held < header.byteLength) break; held = 0; start = Number(view.getBigUint64(0, true)); left = Number(view.getBigUint64(8, true)); continue; } if (pendingSize > 0 && start !== pendingStart + pendingSize) { yield [pendingStart, pending.slice(0, pendingSize)]; pendingSize = 0; } if (pendingSize === 0) pendingStart = start; const taken = Math.min(left, part.byteLength - at, CHUNK_SIZE - pendingSize); pending.set(part.subarray(at, at + taken), pendingSize); pendingSize += taken; start += taken; at += taken; left -= taken; if (pendingSize === CHUNK_SIZE) { yield [pendingStart, pending.slice()]; pendingSize = 0; } } } if (pendingSize > 0) yield [pendingStart, pending.slice(0, pendingSize)]; } /** Patch a spillable copy so writes never truncate an input still read. */ async function patched(existing, segments) { let stored = new ByteSpool(); const budget = new YieldBudget(); try { try { await stored.fill(existing); } catch (err) { if (!isEnoent(err)) throw err; await stored.close(); stored = new ByteSpool(); } for await (const [start, data] of reverseSegments(segments)) { const end = start + data.byteLength; while (stored.size < end) { await budget.run(); await stored.append(new Uint8Array(Math.min(CHUNK_SIZE, end - stored.size))); } await stored.write(start, data); } return await stored.output(); } catch (err) { await stored.close(); throw err; } } /** `source` with its first `count` bytes read off, or null when it ends * first. Mirrors Python's _past. */ async function past(source, count) { const iterator = chunks(source); let held = new Uint8Array(); let left = count; while (left > 0) { const next = await iterator.next(); if (next.done === true) return null; held = next.value.subarray(Math.min(left, next.value.byteLength)); left -= next.value.byteLength - held.byteLength; } return asyncChain([held, iterator]); } /** `source` cut at `limit` bytes, closed once they are out. Mirrors Python's * _limited. */ async function* limited(source, limit) { let left = limit; for await (const piece of chunks(source)) { let chunk = piece; if (left !== null) { chunk = chunk.subarray(0, left); left -= chunk.byteLength; } if (chunk.byteLength > 0) yield chunk; if (left === 0) break; } } /** * INFILE from where `-s` puts it and within `-l`, and the offset its first * byte has; null when xxd cannot seek there. A file seeks: past its end it * dumps nothing, before its start it cannot, and a seek from its end reads it * whole for its size. Stdin cannot seek, so it is read off, and ends too soon * or seeks from its end only to fail. Mirrors Python's _seeked. */ async function seeked(path, read, stdin, seek, limit) { const piped = path === null || isStdin(path); const source = path === null ? resolveSource(stdin) : read(path); if (piped && seek.back) return null; let start; if (!seek.back) { start = seek.offset; } else if (seek.relative) { start = -seek.offset; } else { const kept = []; let size = 0; let total = 0; for await (const chunk of chunks(source)) { total += chunk.length; kept.push(chunk); size += chunk.length; while (kept[0] !== undefined && size - kept[0].length >= seek.offset) { size -= kept[0].length; kept.shift(); } if (kept[0] !== undefined && size > seek.offset) { kept[0] = kept[0].subarray(size - seek.offset); size = seek.offset; } } start = total - seek.offset; if (start < 0) return null; return [limited(asyncChain(kept), limit), start]; } if (start < 0) return null; const after = await past(source, start); if (after === null && piped) return null; return [limited(after ?? singleChunk(new Uint8Array()), limit), start]; } /** * Write the dump (or with -r the bytes) to OUTFILE, reading INFILE first, as * xxd opens it first. Mirrors Python's _write_output. */ async function writeOutput(paths, source, stream, writeBytes, pwriteBytes, render, plain) { const [input, target] = paths; if (input === undefined || target === undefined) return [null, new IOResult()]; const failed = (p, err, exitCode) => [ null, new IOResult({ stderr: ENC.encode(fsErrorLine('xxd', p, err)), exitCode }), ]; const dump = await render(); let segments = new Uint8Array(); let data = new Uint8Array(0); const reading = { failed: false }; try { if (dump === null) { const stored = new ByteSpool(); try { await stored.fill(runStream('xxd', { mode: 'reverse', plain, segments: true }, source)); segments = await stored.output(); } catch (err) { await stored.close(); throw err; } } else data = await primeStream(dump, () => { reading.failed = true; }); } catch (err) { if (!isFsError(err)) throw err; return failed(input, err, readFailExitCode('xxd', err)); } if (dump !== null && !isStdin(input) && input.virtual === target.virtual) { // xxd opens OUTFILE before it reads INFILE: the same file reads empty. await closeQuietly(data); data = new Uint8Array(0); } if (dump === null && pwriteBytes !== null) { try { let wrote = false; for await (const [start, chunk] of reverseSegments(segments)) { await pwriteBytes(target, chunk, start); wrote = true; } if (!wrote) await pwriteBytes(target, new Uint8Array(), 0); } catch (err) { if (!isFsError(err)) throw err; return failed(target, err, OPEN_OUTPUT_EXIT); } finally { await closeQuietly(segments); } return [null, new IOResult()]; } if (dump === null) { try { data = await patched(stream(target), segments); } catch (err) { if (!isFsError(err)) throw err; return failed(target, err, OPEN_OUTPUT_EXIT); } finally { await closeQuietly(segments); } } if (writeBytes === null) { await closeQuietly(data); return [ null, new IOResult({ stderr: ENC.encode('xxd: output is not writable on this backend\n'), exitCode: OPEN_OUTPUT_EXIT, }), ]; } try { await writeBytes(target, data); } catch (err) { if (!isFsError(err)) throw err; return failed(reading.failed ? input : target, err, reading.failed ? readFailExitCode('xxd', err) : OPEN_OUTPUT_EXIT); } finally { await closeQuietly(data); } return [null, new IOResult()]; } /** * xxd's counts read by mirage-rs before any operand is resolved: the flags, or * xxd's refusal of a -c. Mirrors Python's `parse_flags`. */ export function parseFlags(bag) { const fl = new FlagView(bag, specOf('xxd')); let values; try { values = scan('xxd', fl.texts('p', 'c', 'g', 's', 'args_l')); } catch (err) { if (!(err instanceof StreamRefusal)) throw err; return refused(err); } const { cols, group, seek, limit } = values; return { reverse: fl.asBool('r'), plain: fl.asBool('p'), uppercase: fl.asBool('u'), cols, group, seek, limit, }; } /** * xxd over INFILE (or stdin) to OUTFILE (or stdout). A dump replaces * OUTFILE; -r writes into it at the dump's offsets and keeps the bytes around * them, as xxd does, through pwrite where the backend has one, so the stored * bytes are what it writes into and no gap is held here. Deliberate * divergence: xxd opens OUTFILE before it * reads, so an INFILE that is OUTFILE reads empty and a directory INFILE * leaves an empty OUTFILE behind; this reads first and writes once. Mirrors * Python's xxd. */ export async function xxdGeneric(parsed, paths, ctx, stream, writeBytes = null, pwriteBytes = null) { const read = stdinStream(stream, ctx.stdin, true); if (paths.length > 2) throw extraOperandError(CommandName.XXD, paths[2]?.rawPath ?? ''); const { seek, limit, uppercase, plain } = parsed; let source; let base = 0; if (parsed.reverse) { source = paths[0] !== undefined ? read(paths[0]) : resolveSource(ctx.stdin); if (seek.offset !== 0 || (limit ?? 0) > 0) { const after = await past(source, Math.max(seek.offset, 0)); source = limited(after ?? singleChunk(new Uint8Array()), limit !== 0 ? limit : null); } } else { const found = await seeked(paths[0] ?? null, read, ctx.stdin, seek, limit); if (found === null) { return [null, new IOResult({ exitCode: CANNOT_SEEK_EXIT, stderr: ENC.encode(CANNOT_SEEK) })]; } ; [source, base] = found; } const render = async () => { if (parsed.reverse) return null; if (parsed.plain) return plainStream(source, uppercase, parsed.cols); return dumpStream(source, parsed.cols, parsed.group, uppercase, base); }; const target = paths[1]; if (target !== undefined && !isStdin(target)) { return writeOutput(paths, source, stream, writeBytes, pwriteBytes, render, plain); } const io = new IOResult(); return [(await render()) ?? xxdReverseStream(source, io, plain), io]; } function namesOutput(operands) { return operands.length > 1 || operands.some((w) => typeof w !== 'string' && w.pattern !== null); } /** * An xxd line writes its second operand, which a pattern the backend * expands may name too; a quoted name is no pattern. Mirrors Python's * `facts`. */ export function facts(fl, operands) { return new LineFacts({ writes: namesOutput(operands) }); } //# sourceMappingURL=xxd.js.map