@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
383 lines • 16.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 { 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