UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

383 lines • 16.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 { z } from 'zod'; import { AsyncLineIterator } from "../../../io/async_line_iterator.js"; import { ByteSpool } from "../../../io/spool.js"; import { StreamRefusal } from "../../../rs/errors.js"; import { refused } from "../../../rs/scan.js"; import { openStreamSync, runStream } from "../../../rs/stream.js"; import { byteView } from "../../../shell/bytes.js"; import { UsageError } from "../../errors.js"; import { specOf } from "../../spec/builtins.js"; import { FlagView } from "../../spec/flag_view.js"; import { usageExitCode, usageHint } from "../../spec/usage.js"; import { mountKey, mountPrefixOf } from "../../../utils/key_prefix.js"; import { IOResult, materialize } from "../../../io/types.js"; import { PathSpec } from "../../../types.js"; import { fsStrerror, isEisdir, isFsError, isMissingPath, isWalkError } from "../../../errors/fs.js"; import { fsErrorLine } from "../../../errors/render.js"; import { resolvePath } from "../../../utils/path.js"; import { STDIN_HEADER_NAME, STDIN_OPERAND } from "../utils/constants.js"; import { isStdin, stdinStream } from "../utils/stream.js"; import { callerStream } from "../generic_bind/dispatch.js"; import { currentCaller } from "../../../context/session_context.js"; import { concat } from "../../../utils/bytes.js"; import { mountRoot } from "../utils/links.js"; const ENC = new TextEncoder(); const DEC = new TextDecoder('utf-8', { fatal: false }); const BLAKE2B = 'blake2b'; // The bits of a whole BLAKE2b digest. const FULL_BITS = 512; // The *sum command that computes `algorithm`, a hashlib name or `blake2b-N` // for BLAKE2b at N bits. function prog(algorithm) { return algorithm.startsWith(BLAKE2B) ? 'b2sum' : `${algorithm}sum`; } // The digest's BSD-style name, as --tag prints it and -c reads it: `SHA256`, // `BLAKE2b`, `BLAKE2b-256`. function tag(algorithm) { return algorithm.startsWith(BLAKE2B) ? `BLAKE2b${algorithm.slice(BLAKE2B.length)}` : algorithm.toUpperCase(); } async function hashSource(source, algorithm) { const parts = []; const [family = algorithm, bits] = algorithm.split('-'); const config = bits === undefined ? { algorithm } : { algorithm: family, length: bits }; for await (const part of runStream('digest', config, source)) parts.push(part); return DEC.decode(concat(parts)); } async function report(io, prog, path, err) { io.stderr = concat([await materialize(io.stderr), ENC.encode(fsErrorLine(prog, path, err))]); io.exitCode = 1; } /** * One line per operand, its digest asked of `stored` (once `stat` has found a * file there) before the file is read; one that fails is reported and the * rest still print. `read` opens an operand now, inside the command's cache * scope. Mirrors Python's hash_operands. */ export async function hashOperands(paths, read, algorithm, prog, render, stat, stored) { const io = new IOResult(); const pending = []; for (const p of paths) { let digest = null; try { if (stored !== undefined && !isStdin(p)) { await stat?.(p); digest = await stored(p, algorithm); } } catch (err) { if (!isFsError(err)) throw err; await report(io, prog, p, err); continue; } pending.push([p, digest, digest === null ? read(p) : new Uint8Array()]); } async function* output() { for (const [p, digest, source] of pending) { try { yield ENC.encode(render(digest ?? (await hashSource(source, algorithm)), p.rawPath)); } catch (err) { if (!isFsError(err)) throw err; await report(io, prog, p, err); } } } return [output(), io]; } // The last of -b, -t and --tag (which reads in binary mode), as GNU's option // loop leaves its one mode. function readMode(fl) { return fl.typedOrder('binary', 'text', 'tag').at(-1); } // The options GNU refuses outside --check, in the order it checks them. const CHECK_ONLY = [ ['ignore_missing', '--ignore-missing'], ['status', '--status'], ['warn', '--warn'], ['quiet', '--quiet'], ['strict', '--strict'], ]; // Refuse the combinations GNU refuses after its option loop, in its order // (coreutils 9.7). function refuseConflicts(fl, name) { const refuse = (message) => new UsageError(`${name}: ${message}\n${usageHint(name)}`, usageExitCode(name)); const mode = readMode(fl); const check = fl.asBool('check'); const tag = fl.asBool('tag'); if (tag && mode === 'text') throw refuse('--tag does not support --text mode'); if (check && fl.asBool('zero')) { throw refuse('the --zero option is not supported when verifying checksums'); } if (check && tag) throw refuse('the --tag option is meaningless when verifying checksums'); if (check && mode !== undefined) { throw refuse('the --binary and --text options are meaningless when verifying checksums'); } for (const [flag, word] of CHECK_ONLY) { if (!check && fl.asBool(flag)) { throw refuse(`the ${word} option is meaningful only when verifying checksums`); } } } // b2sum's digest for its -l, which the digest engine reads as GNU reads it in // the option loop, or b2sum's refusal of it. Mirrors Python's `_blake2b`. function blake2b(fl) { const value = fl.asText('length'); if (value === undefined) return BLAKE2B; let engine; try { engine = openStreamSync('digest', { algorithm: BLAKE2B, length: byteView(value) }); } catch (err) { if (!(err instanceof StreamRefusal)) throw err; return refused(err); } try { const { bits } = JSON.parse(DEC.decode(engine.metadata())); return bits === FULL_BITS ? BLAKE2B : `${BLAKE2B}-${String(bits)}`; } finally { engine.close(); } } /** * Parse the shared `*sum` flag set against one command's spec; b2sum adds -l * to the set. The flags, or b2sum's refusal of -l; a combination GNU refuses * throws. Mirrors Python's `parse_flags`. */ export function parseFlags(bag, name) { const fl = new FlagView(bag, specOf(name)); const algorithm = name === 'b2sum' ? blake2b(fl) : name.slice(0, -3); if (algorithm instanceof IOResult) return algorithm; refuseConflicts(fl, name); const mode = readMode(fl); return { algorithm, check: fl.asBool('check'), binary: mode === 'binary' || mode === 'tag', tag: fl.asBool('tag'), zero: fl.asBool('zero'), strict: fl.asBool('strict'), ignoreMissing: fl.asBool('ignore_missing'), status: fl.asBool('status'), quiet: fl.asBool('quiet'), warn: fl.asBool('warn'), }; } function hashLine(digest, label, flags) { const terminator = flags.zero ? '\0' : '\n'; if (flags.tag) { return `${tag(flags.algorithm)} (${label}) = ${digest}${terminator}`; } const marker = flags.binary ? '*' : ' '; return `${digest} ${marker}${label}${terminator}`; } // The recorded name resolves against the command's cwd, exactly like GNU // resolves it against the process cwd (a relative `f.txt` in the sums // file names a sibling of wherever `-c` runs, not of the sums file). function checkTarget(filename, cwd, mountPrefix) { const virtual = resolvePath(filename, cwd); return new PathSpec({ virtual, directory: virtual, vfsPath: mountKey(virtual, mountPrefix), resolved: true, rawPath: filename, }); } function countNoun(count, singular, plural) { return count === 1 ? singular : `${String(count)} ${plural}`; } // Read a path through the workspace's dispatcher, on whatever mount holds it: a // checksum list names files anywhere, not on the list's mount. A stdin name // (`-`, /dev/stdin) reads the command's input through `stream`, on the // cursor the list itself reads from. Mirrors Python's dispatched_reader. export function dispatchedReader(dispatch, stream) { const caller = currentCaller(); return (path) => (isStdin(path) ? stream(path) : callerStream(dispatch, caller, path)); } // The stderr line for a checksum list that cannot be read: a directory // reports the literal "read error" (its open succeeds and the read fails), // anything else its strerror. Mirrors Python's `_check_list_error`. function checkListError(name, p, error) { const label = p.rawPath !== '' ? p.rawPath : p.virtual; const detail = isEisdir(error) ? 'read error' : (fsStrerror(error) ?? (error instanceof Error ? error.message : String(error))); return `${name}: ${label}: ${detail}`; } async function hashCheck(stream, p, algorithm, ctx, flags) { const name = prog(algorithm); const listed = ctx.vfs !== undefined ? dispatchedReader(ctx.vfs.dispatch, stream) : stream; // A list read from stdin names files on the mount the command runs on. const mountPrefix = isStdin(p) ? mountRoot(ctx) : mountPrefixOf(p.virtual, p.vfsPath); // GNU quotes its stdin name, which holds a space. const checkLabel = p.rawPath === '-' ? `'${STDIN_HEADER_NAME}'` : p.rawPath !== '' ? p.rawPath : p.virtual; const output = new ByteSpool(); const errors = new ByteSpool(); let verified = 0; let mismatched = 0; let readFailures = 0; let malformed = 0; let parsedAny = false; try { for await (const { line: lineno, parsed } of checkLines(stream(p), algorithm)) { if (parsed === null) { malformed += 1; if (flags.warn) { await checkNote(errors, `${name}: ${checkLabel}: ${String(lineno)}: improperly formatted ` + `${tag(algorithm)} checksum line`); } continue; } parsedAny = true; const [expected, filename, named] = parsed; let digest; try { digest = await hashSource(listed(checkTarget(filename, ctx.cwd.virtual, mountPrefix)), named); } catch (error) { if (!isWalkError(error)) throw error; // GNU --ignore-missing skips only absence; a permission or // transport-shaped failure still reports and fails the check. if (flags.ignoreMissing && isMissingPath(error)) continue; const strerror = fsStrerror(error) ?? (error instanceof Error ? error.message : String(error)); await checkNote(errors, `${name}: ${filename}: ${strerror}`); if (!flags.status) await checkNote(output, `${filename}: FAILED open or read`); readFailures += 1; continue; } if (digest === expected) { verified += 1; if (!flags.status && !flags.quiet) await checkNote(output, `${filename}: OK`); } else { if (!flags.status) await checkNote(output, `${filename}: FAILED`); mismatched += 1; } } // GNU's terminal diagnostics and WARNING block, in its order (pinned // against coreutils 9.7): a file with no properly formatted line is // fatal on its own, even under --status. "No file was verified" means // --ignore-missing left zero OK lines — mismatches included — and // follows the summaries; --status silences it (and the summaries, but // not the per-file strerror lines) while its exit 1 stands. if (!parsedAny) { await checkNote(errors, `${name}: ${checkLabel}: no properly formatted checksum lines found`); return [await output.output(), await errors.output(), 1]; } const nothingVerified = flags.ignoreMissing && verified === 0; if (!flags.status) { if (malformed > 0) { await checkNote(errors, `${name}: WARNING: ${countNoun(malformed, '1 line is', 'lines are')} improperly formatted`); } if (readFailures > 0) { await checkNote(errors, `${name}: WARNING: ${countNoun(readFailures, '1 listed file', 'listed files')} could not be read`); } if (mismatched > 0) { await checkNote(errors, `${name}: WARNING: ${countNoun(mismatched, '1 computed checksum', 'computed checksums')} did NOT match`); } if (nothingVerified) { await checkNote(errors, `${name}: ${checkLabel}: no file was verified`); } } const failed = mismatched > 0 || readFailures > 0 || nothingVerified || (flags.strict && malformed > 0); return [await output.output(), await errors.output(), failed ? 1 : 0]; } catch (err) { await output.close(); await errors.close(); throw err; } } const CheckLine = z .object({ line: z.number().int(), parsed: z.tuple([z.string(), z.string(), z.string()]).nullable(), }) .strict(); async function* checkLines(source, algorithm) { const lines = new AsyncLineIterator(runStream('digest', { algorithm, check_lines: true }, source)); try { for await (const line of lines) yield CheckLine.parse(JSON.parse(DEC.decode(line))); } finally { await lines.discard(); } } async function checkNote(store, line) { await store.append(ENC.encode(`${line}\n`)); } /** * Run a GNU `*sum` over resolved operands. An operand's digest is asked of * `stored` first, the one the mount keeps, once `stat` has found a file * there, and the file is read only when there is none; a list under `-c` is * always verified against the bytes. Mirrors Python's checksum_generic. */ export async function checksumGeneric(parsed, paths, ctx, read, name, stat, stored) { const stream = stdinStream(read, ctx.stdin); if (parsed.check) { const output = new ByteSpool(); const errors = new ByteSpool(); let exitCode = 0; try { // Every operand is its own checksum list: GNU verifies each in turn // and keeps going when one cannot be read; a directory operand reads // as the literal "read error" (pinned on coreutils 9.7). With no // operand the checksum list is stdin. for (const p of paths.length > 0 ? paths : [STDIN_OPERAND]) { let checked; try { checked = await hashCheck(stream, p, parsed.algorithm.split('-')[0] ?? '', ctx, parsed); } catch (error) { if (!isWalkError(error)) throw error; await checkNote(errors, checkListError(name, p, error)); exitCode = 1; continue; } await output.fill(checked[0]); await errors.fill(checked[1]); if (checked[2] !== 0) exitCode = 1; } return [await output.output(), new IOResult({ stderr: await errors.output(), exitCode })]; } catch (err) { await output.close(); await errors.close(); throw err; } } return hashOperands(paths.length > 0 ? paths : [STDIN_OPERAND], stream, parsed.algorithm, name, (digest, label) => hashLine(digest, label, parsed), stat, stored); } //# sourceMappingURL=checksum.js.map