@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
346 lines • 14.4 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 { specOf } from "../../spec/builtins.js";
import { FlagView } from "../../spec/flag_view.js";
import { mountKey } from "../../../utils/key_prefix.js";
import { ByteSpool } from "../../../io/spool.js";
import { IOResult, materialize } from "../../../io/types.js";
import { FileType, PathSpec } from "../../../types.js";
import { readStdinAsync } from "../utils/stream.js";
import { extraOperandError } from "../../spec/usage.js";
import { CommandName } from "../../spec/types.js";
import { READ_FAILURES } from "../../../errors/constants.js";
import { classify } from "../../../errors/classify.js";
import { fsStrerror, isEisdir, isEnoent, isFsError, strerrorOf } from "../../../errors/fs.js";
import { shellQuote } from "../../../utils/quote.js";
import { StreamRefusal } from "../../../rs/errors.js";
import { refused } from "../../../rs/scan.js";
import { openStream, openStreamSync, serveRequests } from "../../../rs/stream.js";
import { byteView, textView } from "../../../shell/bytes.js";
import { linkLocation, linksOf, mountRoot, nameLocation } from "../utils/links.js";
import { CycleError, resolvePath } from "../../../utils/path.js";
function companion(spec, suffix) {
return PathSpec.fromStrPath(spec.virtual + suffix, spec.vfsPath + suffix);
}
/**
* The patch text, or GNU's fatal line when it cannot be had. GNU opens the
* patch file before anything else and gives up on the whole run when it
* cannot (exit 2): an open that fails names the file (`Can't open patch file
* x : ...`), a read that fails does not (`read error : ...`), since only one
* patch file is ever read. Mirrors Python's _load_patch_data.
*/
async function loadPatchData(source, stdin, read) {
if (source === null)
return (await readStdinAsync(stdin)) ?? new Uint8Array();
try {
return await read(source);
}
catch (err) {
if (!isFsError(err))
throw err;
const code = err.code;
if (code !== undefined && READ_FAILURES.has(code)) {
return `patch: **** read error : ${String(fsStrerror(err))}\n`;
}
return `patch: **** Can't open patch file ${shellQuote(source.rawPath)} : ${String(fsStrerror(err))}\n`;
}
}
const ENC = new TextEncoder();
/**
* The files mirage-rs's patch engine reads and writes, whole. The engine
* reads the patch as `-` and names every other file as its report does: the
* ORIGFILE operand as typed, or a name from the patch, which resolves against
* the working directory, as GNU's does; `.orig` and `.rej` after either name
* its companions. It stats the names it picks the file among, makes the
* directories a file it creates needs, and removes a file it empties with
* the directories that leaves empty. A file that is not there, is a directory
* or is a link is the engine's to report; any other failure is the command's.
* Mirrors Python's _PatchFiles.
*/
class PatchFiles {
source;
stdin;
orig;
cwd;
root;
read;
write;
stat;
links;
changes;
chmod;
errors = new ByteSpool();
constructor(source, stdin, orig, cwd, root, read, write, stat, links, changes, chmod) {
this.source = source;
this.stdin = stdin;
this.orig = orig;
this.cwd = cwd;
this.root = root;
this.read = read;
this.write = write;
this.stat = stat;
this.links = links;
this.changes = changes;
this.chmod = chmod;
}
/**
* The file a name the engine gives stands for. A stat or a read keeps the
* name as spelled, so `f/` or `f/.` asks for a directory, as GNU's stat
* does; a change drops the `.` steps a store without empty directories
* cannot walk, unless the name ends naming a directory, and keeps each
* `..` for the walk to take after the links before it, as the system's
* own lookup does. Mirrors Python's `_spec`.
*/
spec(path, change = false) {
if (this.orig !== null) {
const typed = byteView(this.orig.rawPath);
if (path.startsWith(typed)) {
const suffix = textView(path.slice(typed.length));
if (suffix === '')
return this.orig;
const where = this.linked(this.orig);
if (where === null)
return companion(this.orig, suffix);
return PathSpec.fromStrPath(where + suffix, mountKey(where + suffix, this.root));
}
}
let name = textView(path);
const key = mountKey(resolvePath(name, this.cwd), this.root);
if (change && !name.endsWith('/') && !name.endsWith('/.') && name !== '.') {
const parts = name.split('/').filter((part) => part !== '' && part !== '.');
name = (name.startsWith('/') ? '/' : '') + parts.join('/');
}
return PathSpec.fromStrPath(name, key, this.cwd);
}
/**
* Where the link a name ends in stands, null when it ends in none. A name
* with dots walks its parent as the kernel does, each link followed before
* the `..` after it, so `link/../f` looks for `f` beside where `link` leads.
* Mirrors Python's `_linked`.
*/
linked(spec) {
if (this.links === undefined)
return null;
let where;
try {
where = nameLocation(this.links, spec, this.cwd);
if (where !== null && spec.dotted !== null)
where = linkLocation(this.links, spec.dotted);
}
catch (err) {
if (!(err instanceof CycleError))
throw err;
console.debug(`patch: no link ends ${spec.virtual}: ${String(err)}`);
return null;
}
return where !== null && this.links.statAt(where) !== null ? where : null;
}
async kind(path) {
let found;
try {
let spec = this.spec(path);
const real = this.links?.resolve(spec.virtual) ?? spec.virtual;
if (real !== spec.virtual)
spec = PathSpec.fromStrPath(real, mountKey(real, this.root));
found = await this.stat(spec);
}
catch (err) {
if (isEisdir(err))
return { kind: 'dir' };
if (err instanceof CycleError || isEnoent(err) || classify(err) === 'ENOTDIR') {
if (this.linked(this.spec(path)) !== null)
return { kind: 'link' };
console.debug(`patch: nothing stands at ${path}: ${String(err)}`);
return {};
}
throw err;
}
const kind = found.type === FileType.DIRECTORY ? 'dir' : 'file';
return found.mode === null ? { kind } : { kind, mode: found.mode };
}
async change(run, path) {
try {
await run(this.spec(path, true));
}
catch (err) {
if (!isFsError(err))
throw err;
const errno = classify(err);
const error = byteView(strerrorOf(err));
return errno === null ? { error } : { error, errno };
}
return {};
}
async serve(request, data) {
const path = typeof request.path === 'string' ? request.path : '';
if (request.op === 'write') {
await this.write(this.spec(path, true), data);
return { reply: {} };
}
if (request.op === 'stat')
return { reply: await this.kind(path) };
if (request.op === 'mkdir' || request.op === 'unlink' || request.op === 'rmdir') {
return { reply: await this.change(this.changes[request.op], path) };
}
if (request.op === 'chmod') {
const mode = Number(request.mode);
return { reply: await this.change((p) => this.chmod(p, mode), path) };
}
if (request.file !== true) {
const patch = await loadPatchData(this.source, this.stdin, this.read);
if (typeof patch === 'string')
return { reply: { line: byteView(patch) } };
return { reply: { end: true }, data: patch };
}
const spec = this.spec(path);
try {
if (this.linked(spec) !== null)
return { reply: { errno: 'ELOOP' } };
return { reply: { end: true }, data: await this.read(spec) };
}
catch (err) {
if (isEisdir(err))
return { reply: { errno: 'EISDIR' } };
if (isEnoent(err) || classify(err) === 'ENOTDIR')
return { reply: { errno: 'ENOENT' } };
throw err;
}
}
}
// patch's options, -p refused by the patch engine before any file: the flags,
// or patch's fatal -p line, exit 2. Mirrors Python's `parse_flags`.
function parseFlags(bag) {
const fl = new FlagView(bag, specOf('patch'));
const input = fl.raw('i');
const strip = fl.asText('p') ?? null;
try {
openStreamSync('patch', { strip: strip === null ? null : byteView(strip) }).close();
}
catch (err) {
if (!(err instanceof StreamRefusal))
throw err;
return refused(err);
}
return {
strip,
reverse: fl.asBool('R'),
inputPath: input instanceof PathSpec ? input : (fl.asStr('i') ?? null),
forward: fl.asBool('N'),
directories: fl.asList('directory'),
dryRun: fl.asBool('dry_run'),
};
}
/**
* The directory `-d` moves to from `cwd`, or GNU patch's fatal line when it
* cannot move there. It is walked a name at a time, as chdir walks it: each
* entry a `.` or `..` steps out of must be a directory too, and so must the
* last, the one it starts from among them. Each is checked once and in
* order, so the first that is not stops the walk. `stat` refuses a directory
* with EISDIR. Mirrors Python's `_directory`.
*/
async function directory(typed, cwd, root, stat) {
let why = typed === '' ? 'No such file or directory' : '';
const names = typed.startsWith('/') ? [] : cwd.split('/').filter(Boolean);
let checked = 0;
for (const name of typed === '' ? [] : [...typed.split('/'), '.']) {
if ((name === '.' || name === '..') && names.length > checked) {
const target = `/${names.join('/')}`;
try {
await stat(PathSpec.fromStrPath(target, mountKey(target, root)));
why = 'Not a directory';
}
catch (err) {
if (isEisdir(err))
checked = names.length;
else if (classify(err) === 'ENOTDIR')
why = 'Not a directory';
else if (isEnoent(err))
why = 'No such file or directory';
else
throw err;
}
if (why !== '')
break;
}
if (name === '..') {
names.pop();
checked = Math.min(checked, names.length);
}
else if (name !== '' && name !== '.') {
names.push(name);
}
}
if (why === '')
return `/${names.join('/')}`;
return ENC.encode(`patch: **** Can't change to directory ${shellQuote(typed)} : ${why}\n`);
}
/** An operand typed relative, read from the directory `-d` left. */
function moved(spec, cwd, root) {
const typed = spec.rawPath !== '' ? spec.rawPath : spec.virtual;
if (typed.startsWith('/') || typed === '-')
return spec;
return PathSpec.fromStrPath(typed, mountKey(resolvePath(typed, cwd), root), cwd);
}
export async function patchGeneric(paths, ctx, read, write, stat, changes, chmod) {
const parsed = parseFlags(ctx.flags);
if (parsed instanceof IOResult)
return [null, parsed];
if (paths.length > 2)
throw extraOperandError(CommandName.PATCH, paths[2]?.rawPath ?? '');
const root = mountRoot(ctx);
let cwd = ctx.cwd.virtual;
for (const typed of parsed.directories) {
const found = await directory(typed, cwd, root, stat);
if (found instanceof Uint8Array)
return [null, new IOResult({ exitCode: 2, stderr: found })];
cwd = found;
}
// `patch [ORIGFILE [PATCHFILE]]`: the second operand is the patch file,
// ahead of -i, and the first is the one file every hunk goes to in place
// of the names the patch's headers carry, which otherwise resolve against
// the working directory. A `-i` classified as a path keeps its typed
// spelling, which the fatal line names. Mirrors Python's patch_generic.
const input = parsed.inputPath;
let source = paths[1] ??
(input instanceof PathSpec
? input
: input !== null
? PathSpec.fromStrPath(input, mountKey(input, root))
: null);
if (parsed.directories.length > 0) {
paths = paths.map((path) => moved(path, cwd, root));
source = source === null ? null : moved(source, cwd, root);
}
const orig = paths[0] ?? null;
const files = new PatchFiles(source, ctx.stdin, orig, cwd, root, read, write, stat, linksOf(ctx), changes, chmod);
const engine = await openStream('patch', {
strip: parsed.strip === null ? null : byteView(parsed.strip),
reverse: parsed.reverse,
forward: parsed.forward,
dry_run: parsed.dryRun,
file: orig === null ? null : byteView(orig.rawPath),
});
try {
const output = await materialize(serveRequests(engine, files));
const stderr = await files.errors.output();
return [
output.byteLength > 0 ? output : null,
new IOResult({ exitCode: engine.status(), stderr }),
];
}
finally {
engine.close();
}
}
//# sourceMappingURL=patch.js.map