UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

346 lines • 14.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 { 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