@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
1,943 lines • 88.8 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 { RAMAccessor } from "../../accessor/ram.js";
import { publishRead, readVersioned, requireSupported } from "../../cache/context.js";
import { latestFingerprint } from "../../cache/file/io.js";
import { LookupStatus } from "../../cache/index/config.js";
import { buildIndex } from "../../cache/index/factory.js";
import { indexLock } from "../../cache/index/lock.js";
import { ListingCheckStore } from "../../cache/index/ram.js";
import { KeyLock } from "../../cache/lock.js";
import {} from "../../cache/types.js";
import { linkView } from "../../commands/builtin/utils/links.js";
import { dispatchStat, linkFollow, vfsStatPath, virtualFlags, virtualKey, } from "../../commands/builtin/utils/paths.js";
import { CLI, noopAccessor } from "../../commands/cli/types.js";
import { Command } from "../../commands/config.js";
import { ROOT_CWD } from "../../commands/constants.js";
import { STDIN_DASH_COMMANDS, STDIN_DASH_LEADING } from "../../commands/spec/constants.js";
import { flagOccurrences } from "../../commands/spec/flag_view.js";
import { hasInjectedHelp, hasInjectedVersion } from "../../commands/spec/standard.js";
import { CommandSpec } from "../../commands/spec/types.js";
import { awaitsAs, callerSignal, currentCaller, effectiveMountMode, requirePathsWritable, runWithCaller, sessionVisibility, strongestModeUnder, withCaller, } from "../../context/session_context.js";
import { activePipe, pipeBelow } from "../../core/dev/device.js";
import { appendByRewrite, expectOffset, pwriteByStreamedRewrite, refuseTaken, truncateByRewrite, } from "../../core/generic/rewrite.js";
import { ebusy, enotdir, enotsup, isEnoent } from "../../errors/fs.js";
import { withEgress } from "../../io/gateway.js";
import { checkMemory } from "../../io/memory.js";
import { MEMORY_LIMIT } from "../../io/spool.js";
import { closeQuietly, OutputStream, rechunk } from "../../io/stream.js";
import { IOResult, materialize } from "../../io/types.js";
import { activeRecords, commandRecords, runWithRecording, withMountContext, } from "../../observe/context.js";
import { encodeText } from "../../shell/bytes.js";
import { NOT_FOUND_STATUS } from "../../shell/constants.js";
import { DEFAULT_READ_SPEC, FileChangeKind, FileStat, FileType, Limit, ListingVersion, MountMode, PathSpec, ReadPolicy, WritePolicy, } from "../../types.js";
import { Activity } from "../../utils/activity.js";
import { getExtension } from "../../utils/filetype.js";
import { pathVisible } from "../../utils/hidden.js";
import { SystemIdSource } from "../../utils/ids.js";
import { mountKey } from "../../utils/key_prefix.js";
import { norm } from "../../utils/path.js";
import { isUnsatisfiableRange, sliceWindow } from "../../utils/ranges.js";
import { rstripSlash } from "../../utils/slash.js";
import { compareCodePoints } from "../../utils/sort.js";
import { declared, declaredCalls, methodName } from "../../vfs/call.js";
import { WRITE_EFFECTS } from "../../vfs/constants.js";
import { DevVFS } from "../../vfs/dev/dev.js";
import { HistoryViewVFS } from "../../vfs/history/history.js";
import { RAMVFS } from "../../vfs/ram/ram.js";
import { Effect } from "../../vfs/types.js";
import { answered, cliVoice, mountVoice, runHandler, } from "../executor/command/handler.js";
import { HANDED, STORED_OPS } from "./constants.js";
import { beyondWindow, entryAt, folderOf, indexKey, keepListing, keepTree, storedListing, storedStat, storedTree, unprovenMiss, } from "./listing.js";
import { coerceReadPolicy } from "./read_policy.js";
import { coerceWritePolicy, createConditions, writeConditions } from "./write_policy.js";
function cmdKey(name, filetype) {
return `${name}\u0000${filetype ?? ''}`;
}
function writeSource(args) {
const first = args[0];
if (first instanceof Uint8Array)
return first;
if (first !== null &&
typeof first === 'object' &&
Symbol.asyncIterator in first &&
typeof first[Symbol.asyncIterator] === 'function')
return first;
throw new TypeError('write op requires bytes or an async byte source as the first arg');
}
function lengthArg(value) {
if (typeof value !== 'number') {
throw new TypeError('truncate op requires a number length as the first arg');
}
return value;
}
function offsetArg(value, path) {
if (typeof value !== 'number') {
throw new TypeError('pwrite op requires a number offset as the second arg');
}
return expectOffset(value, path);
}
function dstArg(value) {
if (!(value instanceof PathSpec)) {
throw new TypeError('rename op requires a dst PathSpec as the first arg');
}
return value;
}
/** What rides beside the path: the handed stats, and a walk's `limit`. */
function handedOf(kw) {
const handed = kw.handed;
return typeof kw.limit === 'number' ? { ...handed, limit: kw.limit } : handed;
}
/**
* Refuse a keyword the function `name` does not take: the TypeError a direct
* call raises in Python, never silently dropped. The mount hands every
* function its `index` and a caller may name the `filetype`; a built-in takes
* the keywords its Python signature names, and a custom function none, a
* TypeScript function having no keyword parameters. Mirrors Python's `_taken`.
*/
function taken(name, kwargs) {
const keywords = {
readdir: ['limit'],
read_tree: ['limit'],
read: ['offset', 'size'],
write: ['atomic'],
mkdir: ['parents'],
truncate: ['no_create'],
files_containing: ['whole_word', 'ignore_case'],
lines_containing: ['ignore_case'],
before_full_walk: ['files'],
setattr: ['mode', 'uid', 'gid', 'atime', 'mtime'],
};
const known = Object.hasOwn(keywords, name) ? (keywords[name] ?? []) : [];
const [first] = Object.keys(kwargs)
.filter((key) => key !== 'index' && key !== 'filetype' && !known.includes(key) && kwargs[key] !== undefined)
.sort(compareCodePoints);
if (first !== undefined) {
throw new TypeError(`${name}() got an unexpected keyword argument '${first}'`);
}
}
/**
* A backend read's answer as a stream: a promise awaited on the first pull,
* a stream passed through, bytes as one chunk. Mirrors Python's `_as_stream`.
*/
async function* asStream(answer) {
const found = await answer;
if (found instanceof Uint8Array) {
if (found.byteLength > 0)
yield found;
return;
}
yield* found;
}
/**
* A backend read's answer as bytes, a stream collected. A whole read's
* stream publishes the version it recorded for the bytes collected from it,
* as a whole read answering bytes publishes its own, so a read-modify-write
* still sends the version it read. Its records are its own, so a read
* running beside it never lends it a version, and reach the block around it
* too, so the write a rewrite makes still sees the read that proves the copy
* it extends. Bytes answered whole must fit in what the workspace's
* `max_memory` has left unless they are what a RAM store holds (`counted`),
* already counted. Mirrors Python's `_collected`.
*/
async function collected(answer, path, whole, clock, counted = false) {
const found = await answer;
if (found instanceof Uint8Array) {
if (!counted)
checkMemory(found.byteLength);
return found;
}
if (!whole)
return materialize(found);
const [data, records] = await recorded(() => materialize(found), clock);
publishRead(path.virtual, data, latestFingerprint(records, path.virtual));
return data;
}
/**
* Run a whole read's pull with the records it makes its own, which reach the
* block around it too. Mirrors Python's `_recorded`.
*/
async function recorded(pull, clock) {
const held = currentCaller().commandSink;
const outer = held !== null && held[1] !== held[0] ? held[1] : null;
const collect = () => commandRecords(async (records) => {
try {
return [await pull(), records];
}
finally {
outer?.push(...records);
}
});
return activeRecords() === undefined
? (await runWithRecording(collect, null, clock))[0]
: await collect();
}
/**
* A read, honoring a byte window when one is asked for.
*
* A VFS that reads ranges natively fetches only the window, which is the
* whole point on an object store: one ranged GET instead of the whole file.
* Every other read is whole and sliced, which is the only meaningful
* behavior for content that is rendered rather than stored. A zero-length
* read is answered here rather than sent anywhere, and a window starting at
* or past EOF answers empty, the POSIX answer, where an HTTP store refuses
* with 416: normalizing here keeps the op's contract one thing whichever
* path answers it. `counted` says `read` answers bytes a RAM store holds,
* already counted against the workspace's `max_memory`. Mirrors Python's
* `_read_window`.
*/
async function readWindow(read, ranges, counted, path, kwargs, handed, clock) {
const offset = typeof kwargs.offset === 'number' ? kwargs.offset : 0;
const size = typeof kwargs.size === 'number' ? kwargs.size : null;
if (size === 0)
return new Uint8Array(0);
const whole = offset === 0 && size === null;
if (ranges && !whole) {
try {
return await collected(read(path, handed, offset, size), path, false, clock, counted);
}
catch (err) {
if (!isUnsatisfiableRange(err))
throw err;
return new Uint8Array(0);
}
}
const data = await collected(read(path, handed), path, true, clock, counted);
return whole ? data : sliceWindow(data, offset, size);
}
// What the walk guard proves an operand's `.` and `..` with: the
// dispatcher's stat and link follow, bound around the handler. No session
// files (a mount driven directly) binds nothing. Mirrors the Python
// set_walk_probe binding in Mount.run_command.
function walkProbeOf(ctx) {
if (ctx.vfs === undefined)
return null;
return { stat: dispatchStat(ctx.vfs.dispatch), follow: linkFollow(linkView(ctx.vfs)) };
}
/**
* A command tree's root, which never runs: the executor walks the tree and
* runs the leaf the line names (`MountEntry.runLeaf`). Mirrors Python's
* `_tree_root`.
*/
function treeRoot(_accessor, _paths, _texts, ctx) {
throw new Error(`${ctx.spec?.name ?? '?'}: a command tree runs the leaf its walk names`);
}
/**
* The command a mount registers for a command tree: its root, under the
* tree's name. The tree runs on the mount's accessor, so it declares none of
* an installation's own: no config, no accessor, no script, no runtime.
* Mirrors Python's `_tree_command`.
*/
function treeCommand(cli) {
const name = cli.spec.name;
for (const [setting, own] of [
['configModel', cli.configModel !== null],
['accessor', cli.accessor !== noopAccessor],
['script', cli.script !== null],
['runtime', cli.runtime !== null],
]) {
if (own) {
throw new Error(`cli '${name}': a command tree on a mount runs on the mount's accessor; it cannot declare ${setting}`);
}
}
return new Command({
name,
spec: cli.spec,
vfs: null,
filetype: null,
fn: treeRoot,
write: Object.values(cli.handlers).some((handler) => handler.write),
});
}
// The grammar a handler sees at `ctx.spec`, named for the command it runs as
// (python3 or python, js or node). Mirrors Python's _named_spec.
function namedSpec(cmd, cmdName) {
if (cmd.spec.name === cmdName)
return cmd.spec;
const spec = cmd.spec;
return new CommandSpec({ ...spec, name: cmdName });
}
export class MountEntry {
visible = null;
mountId;
prefix;
vfs;
// The backend's own walkLimit, kept from before any mount settles the VFS's.
backendWalkLimit;
// The functions the VFS answers itself, read once when it is mounted: each
// dispatcher call it defines, and `read_range` when a read fetches only the
// range asked for. A command asks its mount's row (`MountView.answers`).
calls;
mode;
read;
write;
// `index` is this same store scoped by the cache manager, which is
// what ops and commands receive.
indexStore;
indexConfig;
inheritsIndex;
vfsRef;
activity = new Activity();
retiring = false;
beforeUse = null;
readyLock = new KeyLock();
/**
* Per-path revision pins installed at Workspace.load time. Read
* functions consult these via the {@link revisionFor} contextvar
* lookup; on a hit, the backend GET pins to the recorded revision so
* replay serves the exact bytes the agent saw. Empty during normal
* runs; populated only by the snapshot loader.
*/
revisions = new Map();
cacheManager = null;
writeContextFor = null;
cmds = new Map();
// The keys of `cmds` a user registered (`registerCommands`), not mirage.
userKeys = new Set();
generalCmds = new Map();
// name -> the root command registered and the tree it walks
trees = new Map();
cmdSpecs = new Map();
commandLimits = new Map();
/**
* The functions the VFS answers itself, read when it is mounted and again
* when its `walkLimit` is settled (a backend answers `files_containing` only
* under a cap), plus `read_range` when a read fetches only the range asked
* for. Mirrors Python's `answered_calls`.
*/
answeredCalls() {
const vfs = this.vfs;
const calls = new Set([...declaredCalls(vfs.constructor).keys()].filter((name) => vfs.supports(name)));
if (vfs.readsRanges)
calls.add('read_range');
return calls;
}
/** The bottom layer of a command's limit on this mount: its backend's `walkLimit`. */
get defaultLimit() {
return new Limit({ walkLimit: this.vfs.walkLimit });
}
// first token -> descending token counts of multi-word command names
// (e.g. "gws docs documents get"); backs longest-prefix command
// resolution. null until first built; invalidated on register.
prefixIndex = null;
constructor(init) {
const prefix = init.prefix;
if (!prefix.startsWith('/')) {
throw new Error(`prefix must start with /: ${prefix}`);
}
if (!prefix.endsWith('/')) {
throw new Error(`prefix must end with /: ${prefix}`);
}
if (prefix.includes('//')) {
throw new Error(`prefix must not contain //: ${prefix}`);
}
for (const [filetype, renderer] of Object.entries(init.vfs.renderers)) {
if (typeof init.vfs[renderer] !== 'function') {
throw new TypeError(`${init.vfs.constructor.name}.renderers maps '${filetype}' to '${renderer}', which is not a method`);
}
}
this.mountId = (init.ids ?? new SystemIdSource()).uuid7();
this.prefix = prefix;
this.vfs = init.vfs;
this.backendWalkLimit = init.vfs.walkLimit;
this.calls = this.answeredCalls();
this.mode = init.mode ?? MountMode.READ;
// A frozen copy carrying the coerced policy, not the caller's object.
//
// Frozen because Python's `ReadSpec` is a frozen dataclass, so the
// same spec cannot be edited after the mount-time verdict passed it;
// a plain JS object can, which would let a caller flip a RAM mount to
// `fresh` behind the verdict's back.
//
// Coerced because `ReadPolicy` is a string-const object: a runtime
// spec carrying `'FRESH'` matches no `===` downstream -- the gate, the
// operand probe -- so the mount would pass its capability check
// and then read as `bounded` everywhere, which is the silent
// downgrade the policy exists to remove. The Python twin normalizes
// at this same point.
const spec = init.read ?? DEFAULT_READ_SPEC;
this.read = Object.freeze({ ...spec, policy: coerceReadPolicy(spec.policy) });
// What the client keeps of its own (a store's search results) is served
// under the same policy.
init.vfs.accessor.govern?.(this.read);
// Coerced so an untyped caller's bare string still matches `===`.
this.write = coerceWritePolicy(init.write);
this.indexStore = init.index ?? buildIndex(undefined, init.vfs.indexTtl);
this.indexConfig = init.indexConfig === undefined ? undefined : { ...init.indexConfig };
this.inheritsIndex = init.inheritsIndex ?? false;
this.vfsRef = init.vfsRef ?? null;
}
/** Whether this mount answers the op `name`. */
answers(name) {
return this.callers(name, null).length > 0;
}
/**
* The VFS as the owner of `access` sees it: the device mount's view of that
* session's descriptors, or the history view of its commands, which a
* browser call cannot find any other way.
*/
viewFor(access) {
if (access === undefined)
return this.vfs;
if (this.vfs instanceof DevVFS || this.vfs instanceof HistoryViewVFS) {
return this.vfs.forSession(access.sessionId);
}
return this.vfs;
}
/** The live pipe a read of `path` takes bytes from, which another task is still writing. Mirrors Python's `live_pipe`. */
livePipe(path, access) {
const accessor = this.viewFor(access).accessor;
if (!(accessor instanceof RAMAccessor))
return null;
return activePipe(accessor, norm('/' + mountKey(path, rstripSlash(this.prefix))));
}
/** Whether a live pipe sits at `path` or below it. Mirrors Python's `holds_live_pipe`. */
holdsLivePipe(path, access) {
const accessor = this.viewFor(access).accessor;
if (!(accessor instanceof RAMAccessor))
return false;
return pipeBelow(accessor, norm('/' + mountKey(path, rstripSlash(this.prefix))));
}
/** Metadata access bound to this mount's ownership. */
get index() {
return this.cacheManager?.scopeIndex() ?? this.indexStore;
}
/** Finish deferred mount preparation before any backend or cache read. */
async use(fn) {
await this.ensureReady();
this.checkActive();
const release = this.activity.acquire();
try {
return await fn();
}
finally {
release();
}
}
async ensureReady() {
this.checkActive();
if (this.beforeUse === null)
return;
await this.readyLock.withLock('', async () => {
if (this.beforeUse !== null) {
await this.beforeUse();
this.beforeUse = null;
}
});
this.checkActive();
}
checkActive() {
if (this.retiring)
throw ebusy(this.prefix);
}
/**
* This mount's mode narrowed by the owner's cap, or the live sessions'
* when no owner is supplied. The configured mode is the ceiling; a
* session's mode can only weaken it.
*/
effectiveMode(access) {
return effectiveMountMode(this.prefix, this.mode, access);
}
// ── command registration ──────────────────────────
register(cmd) {
const key = cmdKey(cmd.name, cmd.filetype);
this.cmds.set(key, cmd);
this.userKeys.add(key);
this.cmdSpecs.set(cmd.name, cmd.spec);
this.prefixIndex = null;
}
registerGeneral(cmd) {
this.generalCmds.set(cmd.name, cmd);
this.cmdSpecs.set(cmd.name, cmd.spec);
this.prefixIndex = null;
}
resolveCommand(cmdName, extension = null) {
if (extension !== null && extension !== '') {
const specific = this.cmds.get(cmdKey(cmdName, extension));
if (specific !== undefined)
return specific;
}
const byVfs = this.cmds.get(cmdKey(cmdName, null));
if (byVfs !== undefined)
return byVfs;
const general = this.generalCmds.get(cmdName);
if (general !== undefined)
return general;
// Fall back to any filetype variant so callers without an extension can
// still find the command; the actual handler is picked by runCommand.
for (const rc of this.cmds.values()) {
if (rc.name === cmdName)
return rc;
}
return null;
}
/**
* Whether a run of `cmdName` on an operand with `extension` can reach a
* handler that declares it may write. Every handler the run's cascade tries
* counts, the filetype one first, since one that declines hands the run to
* the next. The directory probe a run makes (`pickHandlers`) is not made,
* so a directory named like a file counts its filetype handler too.
* `builtins` is false when the line's options leave mirage's own handlers
* nothing to write (`LineFacts.writes`); a user command counts either way.
* Mirrors Python's MountEntry.may_write.
*/
mayWrite(cmdName, extension = null, builtins = true) {
return this.resolveCascade(cmdName, extension, this.cmds, this.generalCmds).some((cmd) => cmd.write && (builtins || this.isUserCommand(cmd)));
}
/**
* How many leading words form a registered command name here. Command
* names may span several words ("gws docs documents get"), git-style.
* Returns the length of the longest registered name that is a prefix of
* `words`, or 1 (the bare first token) if no multi-word name matches; 0
* for no words.
*/
longestCommandMatch(words) {
if (words.length === 0)
return 0;
if (this.prefixIndex === null) {
const index = new Map();
const names = new Set([
...this.cmdSpecs.keys(),
...[...this.cmds.values()].map((rc) => rc.name),
...this.generalCmds.keys(),
]);
for (const name of names) {
const tokens = name.split(' ');
const [first] = tokens;
if (first === undefined || tokens.length <= 1)
continue;
const lengths = index.get(first) ?? new Set();
lengths.add(tokens.length);
index.set(first, lengths);
}
this.prefixIndex = new Map([...index].map(([k, v]) => [k, [...v].sort((a, b) => b - a)]));
}
const [first] = words;
if (first === undefined)
return 1;
for (const length of this.prefixIndex.get(first) ?? []) {
if (length <= words.length &&
this.resolveCommand(words.slice(0, length).join(' ')) !== null) {
return length;
}
}
return 1;
}
allCommands() {
const seen = new Set();
const out = [];
for (const rc of this.cmds.values()) {
if (seen.has(rc.name))
continue;
seen.add(rc.name);
out.push(rc);
}
for (const rc of this.generalCmds.values()) {
if (seen.has(rc.name))
continue;
seen.add(rc.name);
out.push(rc);
}
return out;
}
specFor(cmdName) {
return this.cmdSpecs.get(cmdName) ?? null;
}
commands() {
const result = new Map();
for (const rc of this.cmds.values()) {
const list = result.get(rc.name) ?? [];
list.push(rc.filetype);
result.set(rc.name, list);
}
for (const name of this.generalCmds.keys()) {
if (!result.has(name))
result.set(name, []);
}
return sortFiletypeMap(result);
}
/**
* Whether a ranged read of `path` fetches only that range. False where
* the read that answers it reads the whole file and slices: a VFS with no
* native range, or a rendered filetype.
*/
readsRanges(path) {
return this.vfs.readsRanges && !this.renders(getExtension(path));
}
/** Whether the VFS declares the function `name` a write. */
/**
* Refuse a write the mount's mode no longer grants at any path. Admission
* judged the mode before the call waited for the mount and for its write
* lock; `setMountMode` can make the mount read-only in between, so the mode
* is read again as the backend call starts. A call that reaches a subtree
* (a rename, a tree removal or copy) is refused by a read-only region
* anywhere below its paths, and a copy only reads its source. Mirrors
* Python's `require_writable`.
*/
requireWritable(name, path, values, access) {
const mark = declared(this.vfs.constructor, name);
if (mark === null || !WRITE_EFFECTS.includes(mark.effect))
return;
const others = values.filter((value) => value instanceof PathSpec);
requirePathsWritable(mark.effect === Effect.COPY ? others : [path, ...others], this.prefix, this.mode, mark.subtree, access);
}
/** Refuse a keyword `name` does not take, as a cold call would. Mirrors Python's `refuse_keywords`. */
refuseKeywords(name, kwargs) {
taken(name, kwargs);
}
/**
* Whether the VFS renders a read of `filetype`. A rendered read is never
* served from or kept in the file cache.
*/
renders(filetype) {
return filetype !== null && Object.hasOwn(this.vfs.renderers, filetype);
}
/**
* Batch-register user commands. Mirrors Python's `Mount.register_commands(...)`.
* Each one is this mount's own, keyed by name and filetype, so it wins
* over the shared commands of that name, and is a user command
* (`isUserCommand`): refused before it writes to a read-only one. Multi-VFS entries are filtered as
* `ownCommands` says. A `CLI` serves its verbs as a command tree on this
* mount, under its root's name: a line naming it walks the tree as an
* installed CLI's does and runs the leaf on this mount's accessor
* (`runLeaf`); a tree that declares an installation's own setting throws.
*/
registerCommands(items) {
const [commands, trees] = this.ownCommands(items);
for (const cmd of commands)
this.register(cmd);
for (const [root, cli] of trees)
this.trees.set(root.name, [root, cli]);
}
/**
* Register mirage's own commands for this mount's VFS (a builtin backend's
* table), filtered as `registerCommands` filters; they are not user
* commands. Mirrors Python's `Mount._register_builtins`.
*/
registerBuiltinCommands(items) {
for (const cmd of this.ownCommands(items)[0]) {
const key = cmdKey(cmd.name, cmd.filetype);
this.cmds.set(key, cmd);
this.userKeys.delete(key);
this.cmdSpecs.set(cmd.name, cmd.spec);
this.prefixIndex = null;
}
}
/**
* The entries of `items` this mount registers: multi-VFS entries (sharing
* the same name across mounts) are filtered to this mount's VFS kind; if a
* name has entries but none match this mount, throw.
*/
ownCommands(items) {
const kind = this.vfs.name;
const groups = new Map();
const trees = [];
for (const entry of items) {
let item;
if (entry instanceof CLI) {
item = treeCommand(entry);
trees.push([item, entry]);
}
else {
item = entry;
}
let g = groups.get(item.name);
if (!g) {
g = { toRegister: [], attempted: new Set() };
groups.set(item.name, g);
}
if (item.vfs === null || item.vfs === kind)
g.toRegister.push(item);
else
g.attempted.add(item.vfs);
}
for (const [name, g] of groups) {
if (g.toRegister.length === 0) {
const list = [...g.attempted].sort(compareCodePoints);
throw new Error(`command '${name}' is for VFS(s) [${list.map((r) => `'${r}'`).join(', ')}], not '${kind}'`);
}
}
return [[...groups.values()].flatMap((g) => g.toRegister), trees];
}
/**
* The command tree this mount serves under `cmdName`, null when the name
* resolves to any other command here. Mirrors Python's
* `MountEntry.cli_for`.
*/
cliFor(cmdName) {
const tree = this.trees.get(cmdName);
if (this.resolveCommand(cmdName) !== tree?.[0])
return null;
return tree[1];
}
/**
* What answers `name` on this mount, in the order to try.
*
* A rendered filetype's renderer answers a read before `read` does,
* window and all, and the first answer that is not null wins. The rest
* is the dispatcher's own shape around the VFS's functions: a read takes a
* window, `append` and `pwrite` are a rewrite where the VFS only writes
* whole files, and `mkdir` refuses a taken name first. Only a method
* marked `@vfsCall` is reachable by name, and a custom one is handed the
* scope and the op's positional arguments.
* Mirrors Python's `MountEntry._callers`.
*/
callers(name, filetype, vfs = this.vfs) {
if (name === 'read') {
const levels = [];
const renderer = filetype !== null ? vfs.renderers[filetype] : undefined;
const render = renderer === undefined
? undefined
: vfs[renderer];
if (render !== undefined) {
levels.push((scope, _args, kw) => readWindow(render.bind(vfs), true, false, scope, kw, handedOf(kw), this.indexStore.clock));
}
if (vfs.supports('read')) {
const read = (path, given, offset, size) => vfs.read(path, given, offset, size, callerSignal());
levels.push(async (scope, _args, kw) => {
const data = await readWindow(read, vfs.readsRanges, vfs instanceof RAMVFS, scope, kw, handedOf(kw), this.indexStore.clock);
if (kw.size !== 0)
await this.observe(kw.index ?? this.index, [scope]);
return data;
});
}
return levels;
}
if (name === 'write' && vfs.supports('write')) {
return [(scope, args, kw) => this.writeSource(vfs, scope, writeSource(args), false, kw)];
}
if (name === 'append' && (vfs.supports('append') || vfs.supports('write'))) {
return [(scope, args, kw) => this.writeSource(vfs, scope, writeSource(args), true, kw)];
}
if (name === 'truncate' && !vfs.supports(name)) {
if (!vfs.supports('write'))
return [];
return [
(scope, args, kw) => this.truncateByRewrite(scope, lengthArg(args[0]), kw.no_create === true, kw.index),
];
}
if (name === 'pwrite' && !vfs.supports(name)) {
if (!vfs.supports('write'))
return [];
return [
(scope, args, kw) => this.pwriteByRewrite(scope, writeSource(args), offsetArg(args[1], scope), kw.index),
];
}
if (declared(vfs.constructor, name) === null)
return [];
if (!vfs.supports(name))
return [];
switch (name) {
case 'readdir':
return [(scope, _args, kw) => vfs.readdir(scope, handedOf(kw))];
case 'stat':
return [(scope, _args, kw) => vfs.stat(scope, handedOf(kw))];
case 'pwrite':
return [
(scope, args, kw) => this.pwriteSource(vfs, scope, writeSource(args), offsetArg(args[1], scope), handedOf(kw)),
];
case 'create':
return [(scope, _args, kw) => vfs.create(scope, handedOf(kw))];
case 'mkdir':
return [
(scope, _args, kw) => this.mkdir(scope, kw.parents === true, kw.index, handedOf(kw)),
];
case 'unlink':
return [(scope, _args, kw) => vfs.unlink(scope, handedOf(kw))];
case 'rmdir':
return [(scope, _args, kw) => vfs.rmdir(scope, handedOf(kw))];
case 'rename':
return [(scope, args, kw) => vfs.rename(scope, dstArg(args[0]), handedOf(kw))];
case 'copy':
return [(scope, args, kw) => vfs.copy(scope, dstArg(args[0]), callerSignal(), handedOf(kw))];
case 'dir_copy':
return [(scope, args, kw) => vfs.dirCopy(scope, dstArg(args[0]), handedOf(kw))];
case 'rm_r':
return [(scope, _args, kw) => vfs.rmR(scope, handedOf(kw))];
case 'first_lines':
return [(scope, args, kw) => vfs.firstLines(scope, args[0], handedOf(kw))];
case 'last_lines':
return [(scope, args, kw) => vfs.lastLines(scope, args[0], handedOf(kw))];
case 'line_count':
return [(scope, _args, kw) => vfs.lineCount(scope, handedOf(kw))];
case 'checksum':
return [(scope, args, kw) => vfs.checksum(scope, args[0], handedOf(kw))];
case 'follow':
return [
(scope, args, kw) => vfs.follow(scope, args[0], args[1], handedOf(kw)),
];
case 'read_tree':
return [(scope, _args, kw) => vfs.readTree(scope, handedOf(kw))];
case 'search':
return [(scope, args, kw) => vfs.search(scope, args[0], handedOf(kw))];
case 'files_containing':
return [
(scope, args, kw) => vfs.filesContaining(scope, args[0], { wholeWord: kw.whole_word === true, ignoreCase: kw.ignore_case === true }, handedOf(kw)),
];
case 'lines_containing':
return [
(scope, args, kw) => vfs.linesContaining(scope, args[0], { ignoreCase: kw.ignore_case === true }, handedOf(kw)),
];
case 'before_full_walk':
return [
(scope, _args, kw) => vfs.beforeFullWalk(scope, typeof kw.files === 'number' ? kw.files : null, handedOf(kw)),
];
case 'truncate':
return [
(scope, args, kw) => vfs.truncate(scope, lengthArg(args[0]), kw.no_create === true, handedOf(kw)),
];
case 'setattr':
return [(scope, _args, kw) => vfs.setattr(scope, kw, handedOf(kw))];
default: {
const method = vfs[name];
return method === undefined ? [] : [(scope, args) => method.call(vfs, scope, ...args)];
}
}
}
/**
* A write or append of bytes or a stream, handed to the VFS's `write` /
* `append` as it came, an append by rewrite where it has none. Every
* `write` is told `atomic`; one that collects first already lands whole.
* Mirrors Python's `MountEntry._write_op` / `_append_op`.
*/
async writeSource(vfs, path, data, append, kw) {
try {
if (!append) {
return await vfs.write(path, data, callerSignal(), handedOf(kw), kw.atomic === true);
}
if (vfs.supports('append'))
return await vfs.append(path, data, handedOf(kw), callerSignal());
return await this.appendByRewrite(path, await materialize(data), kw.index);
}
finally {
await closeQuietly(data);
}
}
appendByRewrite(path, data, index = this.index) {
return appendByRewrite((p) => this.readAt(index, p), (request) => this.replaceAt(index, request), (p) => this.statAt(index, p), path, data);
}
/**
* A pwrite handed to the VFS's `pwrite`: bytes as they came, a stream a
* window at a time at increasing offsets, so memory holds one window
* whatever its length. Mirrors Python's `MountEntry._pwrite`.
*/
async pwriteSource(vfs, path, data, offset, handed) {
if (data instanceof Uint8Array)
return vfs.pwrite(path, data, offset, handed, callerSignal());
let at = offset;
let result;
for await (const window of rechunk(data, MEMORY_LIMIT)) {
result = await vfs.pwrite(path, window, at, handed, callerSignal());
at += window.byteLength;
}
if (at === offset) {
result = await vfs.pwrite(path, new Uint8Array(), at, handed, callerSignal());
}
return result;
}
pwriteByRewrite(path, data, offset, index = this.index) {
return pwriteByStreamedRewrite((p, spool) => this.readInto(index, p, spool), (request) => this.replaceAt(index, request), (p) => this.statAt(index, p), path, data, offset);
}
truncateByRewrite(path, length, noCreate, index = this.index) {
return truncateByRewrite((p) => this.readAt(index, p), (request) => this.replaceAt(index, request), path, length, noCreate);
}
async replaceAt(index, request) {
try {
requireSupported(this.vfs.conditionSupport, this.vfs.name, request.path, 'put', request.condition);
if (request.condition.kind === 'any' && !this.vfs.supports('_replace'))
return await this.writeAt(index, request.path, request.body);
return await this.vfs._replace(request, callerSignal(), await this.handedFor('write', request.path, null, index));
}
finally {
await closeQuietly(request.body);
}
}
/** The VFS's read of a whole file, handed what is at its path. */
readAt(index, path) {
return collected(this.answer(this.vfs, index, path), path, true, index.clock, this.vfs instanceof RAMVFS);
}
/**
* Fill `spool` with the VFS's read of a whole file, answering the version
* the read carried, as `readAt` publishes it. Mirrors Python's `_read_into`.
*/
async readInto(index, path, spool) {
const [found, version] = await readVersioned(path, () => this.answer(this.vfs, index, path));
if (found instanceof Uint8Array) {
await spool.fill(found);
return version;
}
const [, records] = await recorded(() => spool.fill(found), index.clock);
return latestFingerprint(records, path.virtual);
}
/**
* What `vfs`'s read of a whole file answers, called with the stats it
* declares. Mirrors Python's `_answer`.
*/
async answer(vfs, index, path, signal) {
return vfs.read(path, await this.handedFor('read', path, null, index), 0, null, signal);
}
/** The VFS's write of a whole file, handed what is at its path and the directory it goes in. */
async writeAt(index, path, data) {
return this.vfs.write(path, data, callerSignal(), await this.handedFor('write', path, null, index));
}
async mkdir(path, parents, index = this.index, handed) {
await refuseTaken((p) => this.statAt(index, p), path, parents);
if (parents && this.wants('mkdir').includes('parent'))
return this.mkdirLevels(path, index);
const made = await this.vfs.mkdir(path, parents, handed);
// A directory made new holds nothing yet.
if (!parents && made instanceof FileStat)
await index.setDir(indexKey(this.prefix, path), []);
return made;
}
/**
* `mkdir -p` on a backend that makes a directory inside its parent's entry:
* one level at a time, from the mount root down. The level above does not
* exist yet when the one below needs it as its parent, so each level's
* create answers its new stat and the index keeps it where the next level
* looks for its parent, with its own listing, which is empty. A level that
* is there already is passed, a directory or not: a file there is ENOTDIR
* for the level below, as mkdir(2) finds. Mirrors Python's `_mkdir_levels`.
*/
async mkdirLevels(path, index) {
const names = path.vfsPath.split('/').filter((name) => name !== '');
const root = rstripSlash(this.prefix);
let made = undefined;
for (let depth = 1; depth <= names.length; depth++) {
const level = this.scope(`${root}/${names.slice(0, depth).join('/')}`);
const key = indexKey(this.prefix, level);
const found = await this.entry(level, index);
if (found !== null) {
if (found.type !== FileType.DIRECTORY)
throw enotdir(level);
continue;
}
const parent = await this.entry(this.folder(level), index);
made = await this.vfs.mkdir(level, false, parent === null ? {} : { parent });
if (made instanceof FileStat) {
await index.addEntry(key, made);
await index.setDir(key, []);
}
}
return made;
}
/** The index key of this mount's root, which no listing names. */
/**
* Whether the VFS lists, so the mount keeps its listings as stats. Mirrors
* Python's `_lists`.
*/
get lists() {
return this.vfs.supports('readdir');
}
get rootKey() {
return rstripSlash(this.prefix) || '/';
}
/**
* The directory holding `scope`, keyed below this mount; a glob word stands
* for the directory it lists, and the root holds itself.
*/
folder(scope) {
const target = scope.pattern !== null ? scope.dir : scope;
if (indexKey(this.prefix, target) === this.rootKey)
return target;
const virtual = rstripSlash(target.virtual);
return this.scope(virtual.slice(0, virtual.lastIndexOf('/')) || '/');
}
/** The stats the VFS's function `name` declares it is handed. */
wants(name) {
return this.vfs.handed[methodName(name)] ?? [];
}
/**
* What is at `scope`, as its parent's listing names it, listing the parent
* when no stored listing does. Under `fresh` the running command's check of
* `scope` already asked the backend, so its answer stands in for the walk;
* a check itself always walks. A miss right after a listing of the parent
* landed, with no listing left now, says only that a check cleared the index
* mid-lookup, so it is looked up once more. Mirrors Python's `_entry`.
*/
async entry(scope, index) {
const key = indexKey(this.prefix, scope);
if (key === this.rootKey || !this.lists)
return null;
if (this.cacheManager !== null && !(index instanceof ListingCheckStore)) {
const probed = this.cacheManager.probedStat(scope);
if (probed !== null)
return probed;
}
const listed = [];
const fetched = new Map([[key, null]]);
const warm = async () => {
const names = await this.listed(this.folder(scope), index, fetched);
listed.push(key);
return names;
};
let row = (await entryAt(index, key, warm)) ?? fetched.get(key) ?? null;
if (row === null &&
listed.length > 0 &&
fetched.has(key) &&
(await index.listDir(folderOf(key), key)).status === LookupStatus.NOT_FOUND) {
row = (await entryAt(index, key, warm)) ?? fetched.get(key) ?? null;
}
if (row !== null)
return row;
if (!fetched.has(key))
return null;
if (await beyondWindow(index, key))
return this.beyond(scope, index);
if (!(await this.unproven(index, key)))
return null;
if (this.asksByPath)
return this.beyond(scope, index);
await index.invalidateDir(folderOf(key));
return (await entryAt(index, key, warm)) ?? fetched.get(key) ?? null;
}
/**
* The row the stored listings answer a stat of `key` with, null for one the
* backend must answer, a miss they do not prove (`unproven`) included.
* Mirrors Python's `_stored_row`.
*/
async storedRow(index, key) {
return this.whole(await storedStat(index, key, (folder) => this.proves(index, folder)));
}
/**
* Whether a complete listing of `folder` proves a name it leaves out absent:
* always, but on a VFS that checks such a name again (`unproven`), only a
* listing the running command fetched. Mirrors Python's `_proves`.
*/
proves(index, folder) {
const checks = this.asksByPath || this.vfs.supports('write');
return !checks || index.listedThisCommand(folder);
}
/** Whether the VFS's stat asks by path, handed no stored row. */
get asksByPath() {
return this.lists && this.wants('stat').length === 0;
}
/**
* Whether the stored listings leave `key` out without proving it absent
* (`unprovenMiss`), on a VFS that checks such a name again: one whose stat
* asks by path (one stat) or one that writes (its folder is listed again),
* where a stale miss would let a write land over a file another client
* made. Mirrors Python's `_unproven`.
*/
async unproven(index, key) {
return (this.asksByPath || this.vfs.supports('write')) && unprovenMiss(index, key);
}
/** Invalidate cached absences disproved by current backend answers. Mirrors Python's _observe. */
async observe(index, scopes) {
if (!this.lists || index instanceof ListingCheckStore)
return;
const paths = new Set();
for (const scope of scopes) {
if (this.revisions.has(scope.virtual) || this.livePipe(scope.virtual) !== null)
continue;
let key = indexKey(this.prefix, scope);
while (key !== this.rootKey && key !== '/') {
paths.add(key);
key = folderOf(key);
}
}
if (paths.size > 0) {
try {
await index.invalidateMissing([...paths].sort(compareCodePoints));
}
catch (err) {
console.warn(`listing observation not kept for ${this.prefix}`, err);
}
}
}
/** Observe existence once without pulling beyond the consumer's demand. */
async *observeStream(scope, index, source) {
let observed = false;
try {
for await (const chunk of source) {
if (chunk.byteLength > 0 && !observed) {
await this.observe(index, [scope]);
observed = true;
}
yield chunk;
}
if (!observed)
await this.observe(index, [scope]);
}
finally {
await closeQuietly(source);
}
}
/**
* What is at a path its folder's stored listing leaves out without proving
* it absent (a window, or a listing another command fetched): the backend's
* stat of it, handed what is at its parent; null when nothing is. Mirrors
* Python's `_beyond`.
*/
async beyond(scope, index) {
const handed = {};
if (this.wants('stat').includes('parent')) {
handed.parent = await this.entry(this.folder(scope), index);
}
const generation = this.cacheManager?.generation;
try {
const row = await this.vfs.stat(scope, handed);
if (this.wants('stat').length === 0)
await this.observe(index, [scope]);
this.noteLive(scope, index, row, generation);
return row;
}
catch (err) {
if (!isEnoent(err))
throw err;
console.debug(`nothing unlisted at ${scope.virtual}: ${String(err)}`);
return null;
}
}
/**
* A directory's children, from its stored listing or listed through the
* backend and kept. Finding the rows a listing is handed lists its parent,
* whose fetch may have proved this listing too (a seed). Mirrors Python's
* `_listed`.
*/
async listed(path, index, fetched) {
const key = indexKey(this.prefix, path);
const glob = path.pattern !== null;
const stored = await storedListing(index, key, glob);
if (stored !== null)
return stored;
const handed = await this.handedFor('readdir', path, null, index);
const seeded = await storedListing(index, key, glob);
if (seeded !== null)
return seeded;
const refill = await index.refillToken();
const listed = await this.vfs.readdir(path, handed);
return keepListing(index, key, listed, refill, fetched);
}
/**
* One level of a subtree read: the directory's listing, as `readdir`
* answers it, each child keyed by its name with the row its `stat` answers
* from that listing. Null where a stat does not come from the listing: a
* mount that keeps none, or whose index keeps no listing live (a zero ttl),
* one under `read: fresh`, whose stats check the backend, a listing the
* index does not hold live once listed, and a child whose row a stat would
* finish (`whole`). A listing cut at the walk's limit answers for the
* children it names, as their stats do. Mirrors Python's `_level`.
*/
async level(virtual, limit, access) {
const index = this.index;
if (!this.lists || this.read.policy === ReadPolicy.FRESH || index.ttl <= 0)
return null;
const names = (await this.call('readdir', virtual, [], limit === undefined ? {} : { limit }, access));
const listing = await index.listDir(indexKey(this.prefix, this.scope(virtual)));
const held = listing.entries ?? listing.partialEntries ?? null;
if (held === null)
return null;
const live = new Set(held);
const rows = await index.getMany(names);
const found = new Map();
for (const child of names) {
const row = live.has(child) ? this.whole(rows.get(child) ?? null) : null;
if (row === null)
return null;
found.set(child.slice(child.lastIndexOf('/') + 1), row);
}
return found;
}
/** A stat of `path` on this mount, as `callKeyed` answers one. Mirrors Python's `_stat`. */
async statAt(index, path) {
const stored = (await this.stored('stat', path, index));
if (stored !== null)
return stored;
return this.vfs.stat(path, await this.handedFor('stat', path, null, index));
}
/**
* The stats the VFS's function `name` declares, each found in the stored
* listings. Mirrors Python's `_handed`.
*/
async handedFor(name, scope, dst, index) {
const wants = this.wants(name);
const handed = {};
if (wants.includes('index')) {
handed.index = index;
// Under a fresh check such a function answers from the check's hints:
// walking for its stats would cost the requests it saves.
if (index instanceof ListingCheckStore)
return handed;
}
for (const stat of HANDED) {
if (!wants.includes(stat))
continue;
if (stat === 'entry')
handed.entry = await this.entry(scope, index);
else if (stat === 'parent')
handed.parent = await this.entry(this.folder(scope), index);
else if (dst !== null) {
const there = this.scope(dst.virtual);
if (stat === 'dstEntry')
handed.dstEntry = await this.entry(there, index);
else
handed.dstParent = await this.entry(this.folder(there), index);
}
}
return handed;
}
/**
* Run one level with the stats its function declares. A stat a stored
* listing handed in may be stale: the backend answering ENOENT for it drops
* the listings it came from, which lists them again, and the level runs once
* more. A level handed nothing is not run again, nor a stat the listings
* found nothing at: the backend agrees with them. `found` is the stats
* already found for this level, null to find them here. Mirrors Python's
* `_run`.
*/
async run(name, call, scope, args, kwargs, index, found = null) {
// A rename's or a copy's destination is its first argument; a function
// is handed a stat there only where it declares one.
const dst = args[0] instanceof PathSpec ? args[0] : null;
const handed = found ?? (await this.handedFor(name, scope, dst, index));
try {
return await call(scope, args, { ...kwargs, handed });
}
catch (err) {
if (!isEnoent(err) ||
!HANDED.some((stat) => handed[stat] != null) ||
(name === 'stat' && handed.entry == null))
throw err;
console.debug(`${name} ${scope.virtual} missed its handed rows, asking again: ${String(err)}`);
}
await this.forget(scope, dst, index);
const again = await this.handedFor(name, scope, dst, index);
return call(scope, args, { ...kwargs, handed: again });
}
/** Drop the listings a stale stat was handed from. Mirrors Python's `_forget`. */
async forget(scope, dst, index) {
const paths = [scope];
if (dst !== null)
paths.push(this.scope(dst.virtual));
for (const path of paths) {
const key = indexKey(this.prefix, path);
await index.invalidateDir(folderOf(key));
await index.invalidateDir(key);
}
}
/**
* A listing or a stat the stored listings answer, null for one the backend
* must. Mirrors Python's `_stored`.
*/
async stored(name, scope, index) {
const key = indexKey(this.prefix, scope);
if (name === 'readdir')
return storedListing(index, key, scope.pattern !== null);
if (name === 'read_tree')
return this.namesOnly ? null : storedTree(index, key);
if (key === this.rootKey)
return null;
const row = await this.storedRow(index, key);
if (row === null || !this.namesOnly)
return row;
return this.cacheManager?.listingTrusted(folderOf(key)) === true ? row : null;
}
/**
* A stored row that answers a stat whole, null for one the backend must
* finish: a file with no size, on a VFS whose stat sizes every file (chroma
* sizes a folder's pages on its first stat). Mirrors Python's `_whole`.
*/
whole(row) {
if (row !== null &&
row.size === null &&
row.type === FileType.FILE &&
this.vfs.sizesAlwaysKnown) {
return null;
}
return row;
}
/**
* Whether a kept listing vouches for its children's names only. A folder's
* version moves when its names change, not when a child is edited in place,
* so under `fresh` a mount whose listings are checked by folder answers a
* child's stat from its listing only when the running command listed it.
* Mirrors Python's `_names_only`.
*/
get namesOnly() {
return (this.read.policy === ReadPolicy.FRESH && this.vfs.listingVersion === ListingVersion.FOLDER);
}
/**
* The VFS's read of `scope` as a stream, called on the first pull and
* handed the stats it declares. Mirrors Python's `_streamed`.
*/
async *streamed(vfs, scope, index, signal) {
yield* asStream(this.answer(vfs, index, scope, signal));
}
/**
* Run `name` on this mount's VFS over a scope already keyed below the
* mount: each level in turn until one answers with something other than
* null. A read resolves its renderer by `kwargs.filetype` when the caller
* names one (null asks for the stored bytes) and by the path's extension
* otherwise. A VFS with no `read` of its own (gdocs, gsheets, gslides)
* stores the rendering, so its stored bytes are the renderer's. A
* `read_tree` of one level (`level: true`) is the mount's own listing with
* each child's row (`level`).
*/
async callKeyed(name, scope, args = [], kwargs = {}, access) {
if (name === 'read_tree' && kwargs.level === true) {
return this.level(scope.virtual, kwargs.limit, access);
}
const named = kwargs.filetype === undefined ? getExtension(scope.virtual) : kwargs.filetype;
const filetype = named === null && !this.vfs.supports('read') ? getExtension(scope.virtual) : named;
const vfs = this.viewFor(access);
const levels = this.callers(name, filetype, vfs);
if (levels.length === 0) {
// A copy only reads its source, so the path it is refused at is the
// destination it would have made.
const dst = args[0];
const copies = declared(this.vfs.constructor, name)?.effect === Effect.COPY;
throw enotsup(this.vfs.name, name, copies && dst instanceof PathSpec ? dst : scope);
}
taken(name, kwargs);
this.requireWritable(name, scope, [...args, ...Object.values(kwargs)], access);
const index = kwargs.index ?? this.index;
const answer = async () => {
let found = null;
if (this.lists && STORED_OPS.has(name)) {
let stored = await this.stored(name, scope, index);
if (stored === null && name === 'readdir') {
// Finding the rows a listing is handed lists its parent, whose
// fetch may have proved this listing too (a seed).
found = await this.handedFor(name, scope, null, index);
stored = await this.stored(name, scope, index);
}
if (stored !== null)
return stored;
}
for (const call of levels) {
const refill = this.lists && (name === 'readdir' || name === 'read_tree')
? await index.refillToken()
: null;
const generation = this.cacheManager?.generation;
const result = await this.run(name, call, scope, args, kwargs, index, found);
found = null;
if (result === null || result === undefined)
continue;
const kept = this.lists ? await this.kept(name, scope, index, result, refill) : result;
if (name === 'stat') {
if (kept instanceof FileStat && this.wants('stat').length === 0) {
await this.observe(index, [scope]);
}
this.noteLive(scope, index, kept, generation);
}
return kept;
}
return null;
};
// One listing of a folder at a time: a reader arriving while another
// lists it waits, then answers from what that one kept.
if (name === 'readdir' && this.lists) {
return indexLock(index, indexKey(this.prefix, scope), answer);
}
return answer();
}
/**
* Keep a current stat the backend answered as the running command's
* probe answer: the cache gate would ask the backend the same question
* again. Only a stat that asked the backend counts: one that takes neither
* an index of its own nor a row the mount hands it, either of which a
* listing filled. A probe's own store records through the probe. Mirrors
* Python's `_note_live`.
*/
noteLive(scope, index, result, generation) {
const manager = this.cacheManager;
if (manager !== null &&
manager.generation === generation &&
result instanceof FileStat &&
!(index instanceof ListingCheckStore) &&
this.lists &&
this.wants('stat').length === 0) {
manager.noteProbed(scope, result);
}
}
/**
* What a listing or a subtree read answered, kept as listings; a listing
* answers its children's paths once kept. Mirrors Python's `_kept`.
*/
async kept(name, scope, index, result, refill = null) {
const key = indexKey(this.prefix, scope);
if (name === 'readdir') {
return keepListing(index, key, result, refill);
}
if (name === 'read_tree' && result instanceof Map) {
const tree = result;
await keepTree(index, key, tree, refill);
return new Map([...tree].filter(([path]) => path !== ''));
}
return result;
}
/**
* The VFS's streamed read of a scope keyed below the mount, framed for later
* pulls. The backend runs on each pull, after this frame is gone, so every
* pull gets what a call gets around it: the caller's context (session,
* recorder, cache scope), the mount's recording context, its write context
* (a read hands its token on only where a write will send it) and its
* revision pins. The mount is held until the stream ends or is closed. A read that
* is neither async nor handed stats is called as the stream opens, so what
* it binds (a pipe) is bound before the path can be released. Mirrors
* Python's `read_stream`.
*/
readStream(scope, access) {
const vfs = this.viewFor(access);
if (this.retiring)
throw ebusy(this.prefix);
const caller = this.onMount(currentCaller());
const signal = callerSignal();
const stream = this.wants('read').length > 0 || vfs.read.constructor.name === 'AsyncFunction'
? this.streamed(vfs, scope, this.index, signal)
: asStream(runWithCaller(caller, async () => vfs.read(scope, {}, 0, null, signal)));
return this.activity.hold(withEgress(withCaller(this.observeStream(scope, this.index, stream), caller)));
}
resolveCascade(name, extension, table, general) {
const levels = [];
if (extension !== null && extension !== '') {
const specific = table.get(cmdKey(name, extension));
if (specific !== undefined)
levels.push(specific);
}
const byVfs = table.get(cmdKey(name, null));
if (byVfs !== undefined)
levels.push(byVfs);
const generalEntry = general.get(name);
if (generalEntry !== undefined)
levels.push(generalEntry);
return levels;
}
// ── execution ─────────────────────────────────────
/**
* What a write through this mount must carry, null when its writes are
* unconditional. Bound by both entry points whatever the policy, so an
* unconditional mount clears a context an outer command's mount set.
* Mirrors Python's `MountEntry.write_context`.
*/
writeContext() {
const manager = this.cacheManager;
if (this.write !== WritePolicy.CONDITIONAL || manager === null)
return null;
// One object per manager, so this mount's frames compare equal.
if (this.writeContextFor?.manager !== manager) {
const vfs = this.vfs;
this.writeContextFor = {
manager,
context: {
vfs: vfs.name,
// Judged at each write: an S3 mount with no endpoint reads the environment.
get conditions() {
return writeConditions(vfs);
},
get creates() {
return createConditions(vfs);
},
readVersion: (path) => manager.readVersion(path),
readVersions: (paths) => manager.readVersions(paths),
drop: (path) => manager.invalidate(path, FileChangeKind.UPDATE),
keep: (path, version) => manager.keepVersion(path, version),
observe: (record) => {
manager.observe(record);
},
lose: (path, version) => {
manager.lose(path, version);
},
},
};
}
return this.writeContextFor.context;
}
/**
* `caller` running on this mount: its records name this mount, its writes
* are checked against this mount's write context, its reads keep this
* mount's revision pins and a connection it opens names this mount.
* Mirrors Python's `MountEntry._on_mount`.
*/
onMount(caller) {
return {
...caller,
recording: caller.recording === null ? null : { ...caller.recording, mountId: this.mountId },
write: { prefix: this.prefix, context: this.writeContext() },
revisions: { map: this.revisions.size > 0 ? this.revisions : null },
vfsMount: this.prefix,
};
}
async runCommand(cmdName, paths, texts, flags, ctx = {}) {
return this.use(async () => {
const handlers = await this.pickHandlers(cmdName, paths, ctx);
if (handlers.length === 0) {
return [
null,
new IOResult({
exitCode: NOT_FOUND_STATUS,
stderr: encodeText(`${cmdName}: command not found`),
}),
];
}
const probe = walkProbeOf(ctx);
const access = ctx.session?.access();
return this.inCommandScope(probe, async () => {
for (const cmd of handlers) {
const user = this.isUserCommand(cmd);
const operands = user ? this.keyedPaths(cmdName, paths) : paths.map(virtualKey);
const runCtx = user
? this.commandCtx(this.keyedFlags(flags), ctx, this.prefix)
: this.commandCtx(virtualFlags(flags), ctx, '');
const refusal = this.commandRefusal(cmdName, cmd, flags, access);
if (refusal !== null)
return [null, refusal];
const result = await this.runOne(cmdName, cmd, operands, texts, { ...runCtx, spec: namedSpec(cmd, cmdName) }, access, paths);
if (result !== null)
return result;
}
return [null, new IOResult()];
});
});
}
/**
* The handlers to try in order.
*
* A filetype handler is selected from the operand's NAME, and a
* directory can carry any extension, so the cascade would hand a
* renderer a directory to read. One stat settles it, and only when a
* handler for this exact extension exists, so a mount with no filetype
* registrations never reaches the probe. The built-in is what a
* directory should get: it owns GNU's `Is a directory` wording, and the
* renderer owns nothing but its own format. The DISPATCHER's stat, not
* the backend's, so a mount root and a namespace-only directory answer
* too; null means neither plane saw anything, in which case the renderer
* reports its own miss. Mirrors Python's MountEntry._pick_handlers.
*/
async pickHandlers(cmdName, paths, ctx) {
let extension = paths.length > 0 && paths[0] !== undefined ? getExtension(paths[0].virtual) : null;
const first = paths[0];
const statPath = vfsStatPath(ctx.vfs);
if (extension !== null &&
extension !== '' &&
first !== undefined &&
statPath !== undefined &&
this.cmds.has(cmdKey(cmdName, extension))) {
const entry = await statPath(first);
if (entry !== null && entry.type === FileType.DIRECTORY)
extension = null;
}
return this.resolveCascade(cmdName, extension, this.cmds, this.generalCmds);
}
/** `p` with this mount's backend key stamped on. */
keyed(p) {
return new PathSpec({
virtual: p.virtual,
directory: p.directory,
pattern: p.pattern,
resolved: p.resolved,
vfsPath: mountKey(p.virtual, rstripSlash(this.prefix)),
rawPath: p.rawPath,
dotted: p.dotted,
walkError: p.walkError,
});
}
/**
* Stamp this mount's backend key onto each path operand, for a handler that
* addresses its backend through the mount's accessor (a user command, a
* command tree's leaf); mirage's own commands get every path keyed by its
* whole virtual path (`virtualKey`) and reach it through their files. A
* stdin `-` routed nowhere, so it rides on whichever mount runs the line,
* beside the operands that chose it. Mirrors Python's
* MountEntry._keyed_paths.
*/
keyedPaths(cmdName, paths) {
const mountPrefix = rstripSlash(this.prefix);
const stdinSlots = STDIN_DASH_COMMANDS.has(cmdName)
? (STDIN_DASH_LEADING.get(cmdName) ?? paths.length)
: 0;
return paths.map((p, index) => index < stdinSlots && p.rawPath === '-'
? new PathSpec({
virtual: `${mountPrefix}/-`,
directory: p.directory,
resolved: p.resolved,
vfsPath: '-',
rawPath: p.rawPath,
})
: this.keyed(p));
}
/**
* Stamp this mount's backend key onto path-shaped flag values so backend
* reads can address them: a single PathSpec (awk -f, tar -f) or a list
* (repeated grep -f, jq's --rawfile pairs). Everything else passes
* through unchanged. Mirrors Python's MountEntry._keyed_flags.
*/
keyedFlags(flags) {
const stampedFlags = { ...flags };
flagOccurrences(stampedFlags).push(...flagOccurrences(flags));
for (const [key, value] of Object.entries(flags)) {
if (value instanceof PathSpec)
stampedFlags[key] = this.keyed(value);
else if (Array.isArray(value) && value.some((item) => item instanceof PathSpec)) {
const items = value;
stampedFlags[key] = items.map((item) => item instanceof PathSpec ? this.keyed(item) : item);
}
}
return stampedFlags;
}
/**
* The one typed bag a handler reads, built here and nowhere else.
*
* A pattern operand travels to the handler whole. The handler resolves it
* once, through its files, which is where the namespace facts (links,
* nested mount roots, a trailing slash) are in view, so expanding here
* would lose what the handler needs. Python's dispatcher never expands
* either. The working directory is keyed below `prefix`: this mount's for a
* handler `keyedPaths` keys the operands of, empty for mirage's own
* commands, whose paths keep their whole virtual path. Mirrors Python's
* MountEntry._command_ctx.
*/
commandCtx(flags, ctx, mountPrefix) {
const prefix = rstripSlash(mountPrefix);
const cwd = (ctx.cwd ?? ROOT_CWD).virtual;
return {
...ctx,
// Said on the files, not only bound in the async context, so a read of
// this run's mount after an await answers for this run.
...(ctx.vfs !== undefined ? { vfs: ctx.vfs.servedBy([this.prefix, this.mode]) } : {}),
stdin: ctx.stdin ?? null,
flags,
cwd: new PathSpec({
virtual: cwd,
directory: cwd,
vfsPath: mountKey(cwd, prefix),
resolved: false,
}),
};
}
/**
* Run `fn` with what a handler's backend calls read bound: the mode the
* command tier's mode guard holds each write to (its own region's mode),
* what the command tier's walk guard proves an operand's `.` and `..`
* with, the recorder's mount, the mount's cache manager and write context,
* and the snapshot revision pins. Mirrors Python's MountEntry._command_scope.
*/
inCommandScope(probe, fn) {
const outer = currentCaller();
return runWithCaller({
...this.onMount(outer),
cache: { manager: this.cacheManager },
mount: [this.prefix, this.mode],
walkProbe: probe === null ? outer.walkProbe : [this.prefix, probe],
}, fn);
}
/**
* Refuse a user handler on a hidden mount, or a writer on a read-only one.
*
* A user handler can reach its accessor directly, so a hidden mount's
* handler must never run, even when an explicit operand or cwd selected it.
* It answers an unknown command without revealing the hidden mount's name.
*
* Mirage's own commands write through the dispatcher, which refuses each
* write in the command's own GNU voice, because only the write knows
* whether a line writes: `gzip -c`, `tar -t` and `split -n 1/2` read a
* read-only mount like any reader, and `gzip f` is refused at the write of
* `f.gz`. A user command (`isUserCommand`) may reach its service some other
* way, which no gate would see, so one that writes is refused here, before
* it runs. strongestModeUnder, not effectiveMode: a mount whose only
* writable region is a show entry still runs it. Only wrapper-owned
* responses (an injected help or version) bypass it: a handler that
* declares `--help` itself is handed the flag and runs. The trailing
* newline is load-bearing: stderr accumulates across a line. `served` is
* the command this mount serves `cmd` under, which says whether it is a
* user command: a tree's root for one of its leaves, which is built for its
* one run and never registered. Mirrors Python's
* MountEntry._command_refusal.
*/
commandRefusal(cmdName, cmd, flags, access, served) {
if (this.isUserCommand(served ?? cmd) && !pathVisible(sessionVisibility(access), this.prefix)) {
return new IOResult({
exitCode: NOT_FOUND_STATUS,
stderr: encodeText(`${cmdName}: command not found\n`),
});
}
const infoOnly = (flags.help === true && hasInjectedHelp(cmd.spec)) ||
(flags.version === true && hasInjectedVersion(cmd.spec));
if (cmd.write &&
this.isUserCommand(served ?? cmd) &&
!infoOnly &&
strongestModeUnder(this.prefix, this.mode, access) === MountMode.READ) {
return new IOResult({
exitCode: 1,
stderr: encodeText(`${cmdName}: read-only mount at ${this.prefix}\n`),
});
}
return null;
}
/**
* Whether `cmd` is a user command: one this mount serves that is not
* mirage's own, registered through `registerCommands` (a VFS's handed
* commands among them) rather than as a builtin. Mirrors Python's
* MountEntry._is_user_command.
*/
isUserCommand(cmd) {
const key = cmdKey(cmd.name, cmd.filetype);
return this.userKeys.has(key) && this.cmds.get(key) === cmd;
}
/**
* Run one handler through the runner a CLI leaf shares, in this mount's
* voice. `typed` are the operands as the line typed them, which a file
* error names. A user
* command that declares a write reaches its backend through its own
* handle, past the dispatcher's per-path invalidation, so this mount drops
* its caches once it has run; mirage's own commands write through the
* dispatcher and drop nothing. Mirrors Python's MountEntry._run_one.
*/
async runOne(cmdName, cmd, paths, texts, ctx, access, typed) {
const accessor = this.viewFor(access).accessor;
return this.runIn(cmdName, ctx, mountVoice(cmdName, typed), { command: cmdName, prefixes: [this.prefix], declared: cmd.limit ?? null }, cmd.write && this.isUserCommand(cmd),
// Deferred, so a handler that throws before it returns a promise is
// still the runner's to word and to drop the caches after.
(runCtx) => Promise.resolve().then(() => cmd.fn(accessor, paths, texts, runCtx)));
}
/**
* Run one leaf of a command tree this mount serves.
*
* The executor walked the line and parsed the leaf's words, as it does an
* installed CLI's; the leaf runs here as a command does: on this mount's
* accessor, in its scope, under the read-only refusal for its own `write`
* and, being a user command that holds the raw handle, the refusal a
* restricting session gets. Its failures speak in a CLI's voice. Mirrors
* Python's `MountEntry.run_leaf`.
*/
async runLeaf(prog, cli, handler, paths, texts, ctx) {
const root = this.resolveCommand(cli.spec.name);
const fn = handler.fn;
if (fn === null || root === null)
throw new Error(`${prog}: no command tree serves it here`);
const leaf = new Command({
name: prog,
spec: ctx.spec ?? cli.spec,
vfs: null,
filetype: null,
fn,
write: handler.write,
limit: handler.limit,
});
return this.use(async () => {
const keyedPaths = this.keyedPaths(prog, paths);
const probe = walkProbeOf(ctx);
const access = ctx.session?.access();
const flags = this.keyedFlags(ctx.flags);
const runCtx = this.commandCtx(flags, ctx, this.prefix);
return this.inCommandScope(probe, async () => {
// No flag here is answered for the leaf: walkLeaf answered the
// tree's own help, and the leaf is handed the rest.
const refusal = this.commandRefusal(prog, leaf, {}, access, root);
if (refusal !== null)
return [null, refusal];
const accessor = this.viewFor(access).accessor;
return this.runIn(prog, runCtx, cliVoice(prog), { command: prog, prefixes: [this.prefix], declared: handler.limit }, handler.write && this.isUserCommand(root), (leafCtx) => answered(Promise.resolve().then(() => fn(accessor, keyedPaths, texts, leafCtx))));
});
});
}
/**
* Run a handler body through the shared runner with what this mount binds
* around it. The runner abandons the promise, it cannot cancel it; the
* context's signal (a background job's kill channel, the line's deadline)
* lets a runtime kill what it spawned (python cancels the task instead).
*/
runIn(name, ctx, voice, producer, writes, call) {
// No hold past an abort: an unmount waits on this mount's activity
// with no grace, so a body, or a renderer of the rows it handed over,
// that ignores the abort is let go rather than waited on. The command's
// walks resolve their walkLimit with its own declared limit.
return runWithCaller({ ...currentCaller(), declared: producer.declared }, () => runHandler(call(ctx), name, producer, voice, {
scope: (result) => wrapMountStreams(result, this.activity),
...(writes ? { drop: () => this.dropCaches() } : {}),
...(ctx.signal !== undefined ? { signal: ctx.signal } : {}),
...(ctx.vfs !== undefined ? { vfs: ctx.vfs } : {}),
}));
}
/**
* Drop what this mount keeps after a write that went past the dispatcher:
* its listings and bodies (`CacheManager.drop`), and the answers its client
* keeps of its own (`Accessor.forget`). Mirrors Python's
* `MountEntry.drop_caches`.
*
* @param bodies drop the bodies too; false when the registry clears the
* whole file cache itself.
*/
async dropCaches(bodies = true) {
if (this.cacheManager !== null)
await this.cacheManager.drop(bodies);
else
await this.indexStore.invalidate();
this.accessor.forget?.();
}
/** The client this mount's VFS reaches its backend with. */
get accessor() {
return this.vfs.accessor;
}
/**
* Run an op on this mount's VFS. A read tries a rendered filetype's
* renderer first, then the VFS's own function; the first answer that is
* not null wins. A caller may name the filetype, and null asks for the
* stored bytes even where the VFS renders the filetype: a
* read-modify-write hands whatever it read straight back to `write`,
* which always stores, so reading a rendered form would store the
* rendering over the file. Mirrors Python's `MountEntry.call`.
*/
async call(name, path, args = [], kwargs = {}, access) {
return this.use(async () => {
const mountPrefix = rstripSlash(this.prefix);
const lastSlash = path.lastIndexOf('/');
const scope = new PathSpec({
virtual: path,
directory: lastSlash > 0 ? path.slice(0, lastSlash + 1) : '/',
vfsPath: mountKey(path, mountPrefix),
});
const effectiveKwargs = {
...kwargs,
...(kwargs.index === undefined ? { index: this.index } : {}),
};
return this.framed(async () => (await this.callKeyed(name, scope, args, effectiveKwargs, access)) ?? null);
});
}
/** `path` as this mount's VFS addresses it. Mirrors Python's _scope. */
scope(path) {
const lastSlash = path.lastIndexOf('/');
return new PathSpec({
virtual: path,
directory: lastSlash > 0 ? path.slice(0, lastSlash) : '/',
vfsPath: mountKey(path, rstripSlash(this.prefix)),
});
}
/**
* Whether this mount's store may copy `src` from `source`'s, store to
* store. Not when this mount's VFS cannot copy from that one, nor when the
* store's object is not what a move's read and write would carry: a cached,
* pinned or rendered source reads other bytes, and a conditional mount on
* either end holds a version for the write or for `mv`'s delete. Asked
* before the copy's deadline starts. Mirrors Python's `can_copy_from`.
*/
async canCopyFrom(source, src) {
return !(!this.vfs.supports('copyFrom') ||
this.write === WritePolicy.CONDITIONAL ||
source.write === WritePolicy.CONDITIONAL ||
source.revisions.has(src) ||
source.renders(getExtension(src)) ||
(source.cacheManager !== null && (await source.cacheManager.isCached(source.scope(src)))));
}
/**
* Copy a file from `source`'s store into this mount's, store to store.
* For `cp` and `mv` between two mounts,
* once the dispatcher has judged `src` as a read and `dst` as a create and
* `canCopyFrom` has said yes. The VFS rejects EXDEV when it cannot make the
* copy after all, and the caller moves the bytes itself. `onCall` receives
* the store's own call once it starts.
* Mirrors Python's `copy_from`.
*/
async copyFrom(source, src, dst, onCall) {
await this.use(() => source.use(() => this.framed(() => {
this.requireWritable('write', PathSpec.fromStrPath(dst), []);
const pending = this.vfs.copyFrom(source.vfs, source.scope(src), this.scope(dst));
onCall?.(pending);
return pending;
})));
}
/**
* Read several whole files on this mount in one request. For the
* dispatcher's read-ahead, once it has walked and
* followed each path; keyed by virtual path, a file left out is read on its
* own later. Mirrors Python's `read_many`.
*/
async readMany(paths, limit) {
return this.use(() => this.framed(() => this.batchRead(paths.map((p) => this.scope(p)), limit)));
}
/**
* Run one backend call as `call` frames its own: as the caller on this
* mount (`onMount`), an answer that streams wrapped to keep that frame and
* `owner`'s egress. Public for the dispatcher, which keys and calls its ops
* itself. Mirrors Python's `_framed`.
*/
framed(work, owner) {
return runWithCaller(this.onMount(currentCaller()), async () => wrapStream(await work(), this.mountId, this.activity, owner));
}
/**
* The VFS's batch read, handed what is at each path and at its folder as
* the stored listings name them, where it declares them. Mirrors Python's
* `_read_many`.
*/
async batchRead(scopes, limit) {
const wants = this.wants('readMany');
const index = this.index;
const handed = [];
for (const scope of scopes) {
const one = {};
if (wants.includes('entry'))
one.entry = await this.entry(scope, index);
if (wants.includes('parent'))
one.parent = await this.entry(this.folder(scope), index);
handed.push(one);
}
const result = await this.vfs.readMany(scopes, limit, handed);
await this.observe(index, scopes.filter((scope) => result.has(scope.virtual)));
return result;
}
/**
* Rank several scopes on this mount as one query. For the dispatcher, once it has walked, followed and
* admitted each scope as it does a `search`'s one. Mirrors Python's
* `search_many`.
*/
async searchMany(paths, query) {
return this.use(() => this.framed(() => this.vfs.searchMany(paths.map((p) => this.scope(p)), query)));
}
}
/**
* Preserve a streaming operation's recording owner, caller and egress after
* its dispatch frame exits.
*/
function wrapStream(result, mountId, activity, owner) {
if (result !== null && typeof result === 'object' && Symbol.asyncIterator in result) {
// The backend's own iterator, which the close reaches first: a pull
// waiting inside it ends only once it is told.
const iterator = result[Symbol.asyncIterator]();
const caller = currentCaller();
const kept = withCaller(withMountContext({ [Symbol.asyncIterator]: () => iterator }, mountId), caller);
// The close, a cleanup request it may send among it, runs as the
// stream's own caller, whoever closes it and however late.
return activity.hold(withEgress(new OutputStream(kept[Symbol.asyncIterator](), awaitsAs(caller, () => closeQuietly(iterator))), owner));
}
return result;
}
// Keep the command's caller on the stream the command handed back, so a
// deferred backend read records under the mount and reads the revisions its
// command bound, as an eager one does. Called inside the mount's command
// scope. Mirrors python's _wrap_mount_streams.
function wrapMountStreams(result, activity) {
const [stream, io] = result;
if (stream === null || stream instanceof Uint8Array)
return [stream, io];
return [activity.hold(withCaller(stream, currentCaller())), io];
}
function sortFiletypeMap(m) {
const out = {};
for (const k of [...m.keys()].sort(compareCodePoints)) {
const list = m.get(k) ?? [];
list.sort((a, b) => {
const aKey = a === null ? 0 : 1;
const bKey = b === null ? 0 : 1;
if (aKey !== bKey)
return aKey - bKey;
const as = a ?? '';
const bs = b ?? '';
return compareCodePoints(as, bs);
});
out[k] = list;
}
return out;
}
//# sourceMappingURL=mount.js.map