UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

284 lines • 11.6 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 { chunks } from "../../../io/cooperative.js"; import { OutputStream } from "../../../io/stream.js"; import { StreamRefusal } from "../../../rs/errors.js"; import { refused, scan } from "../../../rs/scan.js"; import { closeStream, mergeOutput, openStream } from "../../../rs/stream.js"; import { byteView } from "../../../shell/bytes.js"; import { specOf } from "../../spec/builtins.js"; import { FlagView } from "../../spec/flag_view.js"; import { IOResult, materialize } from "../../../io/types.js"; import { formatFsError } from "../../../errors/render.js"; import { isEisdir, isFsError } from "../../../errors/fs.js"; import { STDIN_OPERAND } from "../utils/constants.js"; import { isStdin, stdinStream } from "../utils/stream.js"; import { extraOperandError, missingOperandError } from "../../spec/usage.js"; import { CommandName } from "../../spec/types.js"; const ENC = new TextEncoder(); /** * cmp's counts read by mirage-rs before any operand is resolved: -n, -i, -l * and -s in the order typed, so the first refusal is the one diffutils makes * while it reads its options, then the SKIP1 and SKIP2 operands of `texts`. * The flags, or cmp's refusal, exit 2. A count past 2**53 is the nearest * double, which no file reaches. Mirrors Python's `parse_flags`. */ export function parseFlags(bag, texts = []) { const fl = new FlagView(bag, specOf('cmp')); const typed = fl.texts('bytes', 'ignore_initial', 'verbose', 'silent', 'quiet'); for (const text of texts.slice(0, 2)) typed.push(['skip', text]); let values; try { values = scan('cmp', typed); } catch (err) { if (!(err instanceof StreamRefusal)) throw err; return refused(err); } const { limit, skip } = values; const [first, second] = Array.isArray(skip) ? skip : []; return { silent: fl.asBool('quiet') || fl.asBool('silent'), verbose: fl.asBool('verbose'), limit: typeof limit === 'number' ? limit : null, printBytes: fl.asBool('print_bytes'), skip: [typeof first === 'number' ? first : 0, typeof second === 'number' ? second : 0], }; } /** One operand's bytes as cmp pulls them, its first chunk read on open, * which is where an unreadable operand fails. Mirrors Python's _Operand. */ class Operand { held = null; source; constructor(source, signal) { this.source = chunks(source, signal); } async open() { const next = await this.source.next(); this.held = next.done === true ? new Uint8Array() : next.value; } /** The next chunk, null at the end. */ async next() { const held = this.held; this.held = null; if (held !== null) return held.byteLength > 0 ? held : null; const next = await this.source.next(); return next.done === true ? null : next.value; } async close() { await this.source.return?.(); } } /** * Whether `-s` can answer 1 from two regular files' sizes, unread: diffutils * does when the bytes left past each skip differ and the shorter run ends * within `-n`. A stdin operand, or a size the stat does not give, leaves it to * the read, as does a stat that fails, which the read then reports. Mirrors * Python's _sizes_differ. */ async function sizesDiffer(sizeOf, paths, skip, limit) { const left = []; for (const [index, p] of paths.entries()) { if (isStdin(p)) return false; let size; try { size = await sizeOf(p); } catch (err) { if (!isFsError(err)) throw err; console.debug(`cmp: no size for ${p.virtual}: ${String(err)}`); return false; } if (size === null) return false; left.push(Math.max(size - (skip[index] ?? 0), 0)); } const [a = 0, b = 0] = left; return a !== b && (limit === null || Math.min(a, b) < limit); } /** * cmp's answer for two opened operands: the mirage-rs stream skips, compares * and words it, and asks for the operand it needs next, so without -l the * reads stop at the first difference. Mirrors Python's _compare. */ async function compare(operands, config, signal) { const stream = await openStream('cmp', config); const io = new IOResult(); const output = async function* () { try { yield* mergeOutput(stream, async (index) => (await operands[index]?.next()) ?? null, signal); } finally { io.exitCode = stream.status(); const stderr = stream.takeStderr(); io.stderr = stderr.byteLength > 0 ? stderr : null; } }; const produced = output(); const result = new OutputStream(produced, async () => { try { await produced.return(undefined); await closeStream(stream); } finally { for (const operand of operands) await operand.close(); } }); if (config.verbose !== true) { const bytes = await materialize(result); return [bytes.byteLength > 0 ? bytes : null, io]; } return [result, io]; } /** * Both operands naming the one stdin, as diffutils 3.10 answers it for stdin * redirected from a regular file. * * One name at one skip is equal unread. Otherwise cmp skips on the one * descriptor twice, so the files sit at the first skip and at the sum of * both, and are equal unread when those match. -s then answers 1 unread when * the bytes left past each position differ within -n. Otherwise the first * file reads what is left past both skips and the second reads nothing, and * closing the descriptor a second time fails: that line and exit 2 follow * whatever the comparison said. A pipe fails both seeks, which GNU takes for * one position, so it answers 0 where this answers as the file. Mirrors * Python's _one_stdin_twice. */ async function oneStdinTwice(read, p, skip, parsed, options) { if (skip[0] === skip[1] || skip[1] === 0) return [null, new IOResult()]; const data = await materialize(read(p)); const left = [ Math.max(data.byteLength - skip[0], 0), Math.max(data.byteLength - skip[0] - skip[1], 0), ]; if (parsed.silent && left[0] !== left[1]) { if (parsed.limit === null || Math.min(...left) < parsed.limit) { return [null, new IOResult({ exitCode: 1 })]; } } const [out, io] = await compare([new Operand(data.slice(skip[0] + skip[1])), new Operand(new Uint8Array())], { ...options, names: ['-', '-'], piped: [true, true], skip: [0, 0] }); const stdout = out === null ? null : await materialize(out); const held = io.stderr === null ? new Uint8Array() : await materialize(io.stderr); const tail = ENC.encode('cmp: -: Bad file descriptor\n'); const stderr = new Uint8Array(held.byteLength + tail.byteLength); stderr.set(held); stderr.set(tail, held.byteLength); io.stderr = stderr; io.exitCode = 2; return [stdout, io]; } /** * Compare two files byte by byte, GNU cmp semantics. Each file is streamed * and the reads stop at the first difference, or at `-n`; only `-l`, which * lists every difference, reads both to the end. Under `-s` two regular files * whose sizes past their skips differ answer 1 unread, as diffutils does. * Mirrors Python's cmp_cmd. */ export async function cmpGeneric(parsed, paths, texts, ctx, stream, sizeOf, lengthOf = sizeOf) { const p0 = paths[0]; if (p0 === undefined) throw missingOperandError(CommandName.CMP, null, ctx.argv ?? []); const extra = texts[2]; if (extra !== undefined) throw extraOperandError(CommandName.CMP, extra); const skip = parsed.skip; // A lone FILE1 is compared with stdin, which GNU names `-`. const p1 = paths[1] ?? STDIN_OPERAND; const read = stdinStream(stream, ctx.stdin, true); const options = { silent: parsed.silent, verbose: parsed.verbose, limit: parsed.limit, print_bytes: parsed.printBytes, }; if (isStdin(p0) && isStdin(p1)) return oneStdinTwice(read, p0, skip, parsed, options); const same = p0.virtual === p1.virtual && skip[0] === skip[1]; if (parsed.silent && sizeOf !== undefined && !same && (await sizesDiffer(sizeOf, [p0, p1], skip, parsed.limit))) { return [null, new IOResult({ exitCode: 1 })]; } // GNU cmp reserves exit 1 for "files differ"; trouble is exit 2. // diffutils 3.10 opens both operands before it reads either, and -s drops // the message only for an operand it cannot open: a directory opens, fails // at its first read, and is reported whatever -s says, unless both operands // name it, which is the same file at the same offset and so equal unread. // Opening is the first chunk's pull. const operands = []; try { let unread = null; for (const p of [p0, p1]) { const operand = new Operand(read(p), ctx.signal); operands.push(operand); try { await operand.open(); } catch (err) { if (isEisdir(err)) { unread ??= err; continue; } if (!isFsError(err)) throw err; const stderr = parsed.silent ? null : formatFsError('cmp', err, paths); return [null, new IOResult({ exitCode: 2, stderr })]; } } if (same) return [null, new IOResult()]; if (unread !== null) return [null, new IOResult({ exitCode: 2, stderr: formatFsError('cmp', unread, paths) })]; const sizes = [null, null]; if (parsed.verbose && lengthOf !== undefined) { for (const [index, path] of [p0, p1].entries()) { if (!isStdin(path)) { try { sizes[index] = await lengthOf(path); } catch (err) { if (!isFsError(err)) throw err; console.debug(`cmp: no size for ${path.virtual}: ${String(err)}`); } } } } const [first, second] = operands; if (first === undefined || second === undefined) throw new Error('cmp inputs were not opened'); const result = await compare([first, second], { ...options, names: [byteView(p0.rawPath), byteView(p1.rawPath)], piped: [isStdin(p0), isStdin(p1)], skip: [skip[0], skip[1]], sizes, }, ctx.signal); operands.length = 0; return result; } finally { for (const operand of operands) await operand.close(); } } //# sourceMappingURL=cmp.js.map