UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

1,617 lines • 70 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 { readerOf } from "../workspace/files.js";
import { ChunkedHandle, FileTable, READ, overlaid, runGroups, runStream, windowWrites, writeRuns, } from "../runtime/handles/index.js";
import { ByteSlice, ByteSpool, MEMORY_LIMIT } from "../io/spool.js";
import { materialize } from "../io/types.js";
import { READ_CHUNK } from "../runtime/handles/constants.js";
import { ConcurrencyLimiter } from "../concurrency/limiter.js";
import { FSTAT_ROW_SECONDS, LISTING_CONCURRENCY } from "./constants.js";
import { classify } from "../errors/index.js";
import { FileStat, FileType, LIVE_KEY, PathSpec } from "../types.js";
import { parent, posixNormpath } from "../utils/path.js";
import { rstripSlash } from "../utils/slash.js";
import { compareCodePoints } from "../utils/sort.js";
import { atimeMs, contentSize, deviceRdev, isDir, isLink, mtimeMs, posixMode, } from "../utils/stat_view.js";
import { callsAs, currentCaller, runWithSession } from "../context/session_context.js";
import { skippedAtDispatch } from "../policy/match/rule.js";
import { ebadf, eexist, einval, eisdir, enoent, enotdir, isMissingOp, isMissingPath, } from "../errors/fs.js";
import { concat, findBytes } from "../utils/bytes.js";
import { isMacosMetadata } from "./platform/macos.js";
// An entry a listing's stat no longer finds went between the two.
const VANISHED = new Set(['ENOENT', 'ENOTDIR']);
// One cap for the listings that run at once, as Python keeps one per loop.
const STATS = new ConcurrencyLimiter(LISTING_CONCURRENCY);
/**
 * Protocol-neutral mount logic shared by every kernel adapter.
 *
 * Everything here is expressed in POSIX terms (attribute records, ordinary
 * thrown errors) and imports nothing from `@zkochan/fuse-native`, so it is
 * reusable by a non-FUSE adapter and unit-testable without a kernel.
 *
 * The division of labour: this class decides *what* the filesystem
 * contains, an adapter decides *how* to say it to a particular kernel
 * interface. Adapters translate the errors thrown here into their own
 * error codes with `classifyErrno`. Mirrors Python's `MountCore`.
 *
 * Every op goes through `ws.vfs`, which delegates to the dispatcher, so
 * a mount walks the same dispatcher as a shell line (mount modes, policies,
 * cache, invalidation) and every op it runs lands on the `ws.vfs.records`
 * ledger. Reaching `ws.dispatch` from here instead would skip the record;
 * reaching a backend directly would skip the dispatcher.
 */
export class MountCore {
    files;
    session;
    now;
    root;
    handles = new FileTable();
    // The write runs `folded` grew, whose buffers have room past them.
    static grown = new WeakSet();
    // Per file identity and view (stored or rendered): the hydration read out
    // (opens share it) and a generation a change under it bumps. Per identity:
    // the chain of mutations, the chain of removals, and the opens out that a
    // removal waits for.
    hydrations = new Map();
    hydrationGen = new Map();
    pending = new Map();
    removals = new Map();
    opening = new Map();
    uid;
    gid;
    // The mounts that refused an append, which land one as a write.
    noAppend = new Set();
    // Names the files whose name went, one per removal.
    removed = 0;
    constructor(files, options = {}) {
        this.files = files;
        this.now = new Date();
        this.root = options.rootPrefix !== undefined ? rstripSlash(options.rootPrefix) : '';
        this.uid = options.owner?.uid ?? 0;
        this.gid = options.owner?.gid ?? 0;
        this.session = options.session ?? null;
        const skipped = this.session === null ? [] : skippedAtDispatch(this.session.commands);
        if (this.session !== null && skipped.length > 0) {
            // This entry point sees ops, never a line, so the profile's command-level
            // rules have nothing here to judge.
            console.warn(`session ${this.session.sessionId}: an entry point that sees only ops (a kernel mount, ` +
                `SFTP, codex-exec's file calls) cannot apply ${skipped.join('; ')}; path rules, ` +
                'hides and modes still hold');
        }
    }
    // ── helpers ──────────────────────────────────────────────────────
    /**
     * Run one op under the bound session's mount grants and profile, as the
     * Python MountCore binds each op: dispatch reads the session from
     * context, so binding at the op entry point is what scopes a kernel or
     * SFTP mount to it. Unbound, an op runs as whatever session is current.
     */
    op(fn) {
        return this.session === null ? fn() : runWithSession(this.session, fn);
    }
    /** Judge retained metadata through the stat policy boundary. */
    async statCached(path, row) {
        const [result] = await this.op(() => this.files.dispatch('stat', PathSpec.fromStrPath(this.resolve(path), undefined, '/'), [], {
            retained: row,
        }));
        return result;
    }
    /** Judge retained bytes through the same read boundary as a fetch. */
    async readCached(path, data) {
        const [result] = await this.op(() => this.files.dispatch('read', PathSpec.fromStrPath(this.resolve(path), undefined, '/'), [], {
            retained: data,
        }));
        return result;
    }
    resolve(path) {
        if (this.root === '')
            return path;
        return path === '/' ? this.root : this.root + path;
    }
    /**
     * The attrs for one stat row, the way a guest's stat reads it. The row
     * carries the namespace overlay (chmod bits, chown ids, a touched mtime),
     * so what a metadata op stored is what the mount shows, and a device
     * keeps its type and numbers. String uid/gid (names) fall back to the
     * mounting user: the kernel wants numbers and there is no user db to map
     * against. A missing stamp falls back to the mount's start time; epoch
     * zero is a real time and lands. `size` replaces the row's, from an open
     * handle or a link's shown target. Mirrors Python's `MountCore.attrs`.
     */
    attrs(s, size = null) {
        const mtime = mtimeMs(s);
        const when = mtime === null ? this.now : new Date(mtime);
        const atime = atimeMs(s);
        return {
            mtime: when,
            atime: atime === null ? when : new Date(atime),
            ctime: when,
            nlink: isDir(s) ? 2 : 1,
            size: size ?? contentSize(s),
            mode: posixMode(s),
            uid: typeof s.uid === 'number' ? s.uid : this.uid,
            gid: typeof s.gid === 'number' ? s.gid : this.gid,
            rdev: deviceRdev(s),
        };
    }
    /**
     * The target to present for a link at a mount path. Relative targets are
     * stored verbatim and returned as-is. Absolute targets name virtual
     * paths, so they are rewritten relative to the link's directory: returned
     * raw, the kernel would resolve them against the host root and escape
     * the mountpoint.
     */
    shownTarget(path, target) {
        if (!target.startsWith('/'))
            return target;
        let virtualTarget = target;
        if (this.root !== '') {
            if (target === this.root) {
                virtualTarget = '/';
            }
            else if (target.startsWith(this.root + '/')) {
                virtualTarget = target.slice(this.root.length);
            }
            else {
                // points outside the scoped root: unreachable through this
                // mount, keep the stored form (a dangling link is legal)
                return target;
            }
        }
        const slash = path.lastIndexOf('/');
        const from = (slash <= 0 ? '' : posixNormpath(path.slice(0, slash))).split('/').filter(Boolean);
        const to = posixNormpath(virtualTarget).split('/').filter(Boolean);
        let shared = 0;
        while (shared < from.length && from[shared] === to[shared])
            shared++;
        // A target that is the link's own directory is `.`, as Python's relpath
        // names it.
        return [...from.slice(shared).map(() => '..'), ...to.slice(shared)].join('/') || '.';
    }
    /**
     * The length of the bytes an open handle on the file holds. A size-unknown
     * file is read whole when it opens, so while a handle is open its length
     * answers a stat by path too (`ls -l` beside a `cat`). Once every handle
     * is released, the dispatcher answers from the workspace cache instead.
     * Mirrors Python's `held_size`.
     */
    heldSize(path) {
        for (const ctx of this.openOn(this.identity(path))) {
            if (ctx.data !== undefined)
                return ctx.data.byteLength;
        }
        return null;
    }
    /**
     * Read a size-unknown file whole for the handle opening it, through the
     * dispatcher, so a caching mount keeps the bytes for the next open and for
     * a stat once this one closes. Undefined when the read fails: open() stays
     * permissive and the read() that follows surfaces the error. Opens of one
     * view of one file while a read is out share it; `raw` is the stored
     * bytes, for a handle that writes, rather than the rendering. Mirrors
     * Python's `_hydrate`.
     */
    hydrate(path, raw) {
        const key = MountCore.view(this.identity(path), raw);
        const inflight = this.hydrations.get(key);
        if (inflight !== undefined)
            return inflight;
        const promise = (async () => {
            try {
                for (;;) {
                    const gen = this.hydrationGen.get(key) ?? 0;
                    const data = await this.op(() => this.files.read(this.resolve(path), MountCore.stored(raw)));
                    // The file changed while this read was out: what came back is
                    // stale, so read again rather than hand it over.
                    if ((this.hydrationGen.get(key) ?? 0) !== gen)
                        continue;
                    return data;
                }
            }
            catch (err) {
                console.debug(`fuse: hydration read of ${path} failed, deferring to read(): ${String(err)}`);
                return undefined;
            }
            finally {
                this.hydrations.delete(key);
                this.hydrationGen.delete(key);
            }
        })();
        this.hydrations.set(key, promise);
        return promise;
    }
    /**
     * Run one mutation of a file after every mutation already queued for
     * it, and let the next one wait for it. Serialization is per identity,
     * so a flush through a link and a truncate through its target queue
     * behind each other, and a flush still landing cannot be overtaken by a
     * truncate that would then be undone when the flush completes.
     */
    mutate(key, fn) {
        return this.queue(this.pending, key, fn);
    }
    /**
     * Run `fn` holding the chain of every key, taken in sorted order so two
     * callers that hold the same pair never wait on each other. Mirrors
     * Python's `_queue_all`.
     */
    queueAll(queues, keys, fn) {
        const [first = '', ...rest] = [...new Set(keys)].sort(compareCodePoints);
        if (rest.length === 0)
            return this.queue(queues, first, fn);
        return this.queue(queues, first, () => this.queueAll(queues, rest, fn));
    }
    /**
     * Run `fn`, which removes, replaces or moves the files at `paths`, after
     * the opens of those names already out, with later ones held back until
     * it is done, as the kernel orders an open and an unlink or a rename of
     * one name. `hold` reads the rest for the handles open before; an open
     * that slipped in while that read was out would get a chunked handle onto
     * bytes about to go. Chains of their own, taken before any `pending` one,
     * rather than `pending` itself: an open they wait for may be truncating
     * there.
     */
    removing(paths, fn) {
        const keys = paths.map((path) => this.identity(path, false));
        return this.queueAll(this.removals, keys, async () => {
            await Promise.all(keys.flatMap((key) => [...(this.opening.get(key) ?? [])]));
            await fn();
        });
    }
    /**
     * Run `fn` after every call already queued under `key` in `queues`, and
     * let the next one wait for it, whether it resolves or throws.
     */
    queue(queues, key, fn) {
        const prev = queues.get(key) ?? Promise.resolve();
        const run = prev.then(fn, fn);
        const tail = run.then(() => undefined, () => undefined);
        queues.set(key, tail);
        void tail.then(() => {
            if (queues.get(key) === tail)
                queues.delete(key);
        });
        return run;
    }
    /**
     * Land write runs on the mount, one pwrite each, in order. A pwrite keeps
     * every stored byte the handle did not write, so nothing is read through
     * the dispatcher first: a session that may write a file and not read it writes
     * through FUSE, as through a write-only descriptor. An appending handle's
     * runs land as appends instead, at the end the file has then, so what
     * another writer appended meanwhile stays before them, as O_APPEND keeps
     * it. The runs that landed leave `runs` in one step, so after a failure
     * `runs` holds only what did not land and a retry never replays a run over
     * bytes another writer has since put there. A run that fails still
     * invalidates what the core holds, since the runs before it have landed.
     */
    async applyWrites(path, runs, append = false) {
        const target = this.resolve(path);
        let landed = 0;
        try {
            for (const [offset, data] of runs) {
                if (append)
                    await this.appendTo(target, data);
                else
                    await this.op(() => this.files.pwrite(target, data, offset));
                landed += 1;
            }
        }
        finally {
            runs.splice(0, landed);
            await this.changed(path);
        }
    }
    /**
     * Extend a file by `data` at the end it has now. A mount with no append
     * (S3) takes a write of the whole file, read fresh from the backend
     * rather than from a copy, so a write another action made since stays; a
     * mount that declined once is remembered. Mirrors Python's `_append`.
     */
    async appendTo(target, data) {
        const mount = this.files.mountOf(target) ?? target;
        if (!this.noAppend.has(mount)) {
            try {
                await this.op(() => this.files.append(target, data instanceof Uint8Array ? data : runStream(data)));
                return;
            }
            catch (err) {
                if (!isMissingOp(err, 'append'))
                    throw err;
                this.noAppend.add(mount);
            }
        }
        const bytes = data instanceof Uint8Array ? data : await materialize(runStream(data));
        let base = new Uint8Array();
        try {
            const [stored] = await this.op(() => this.files.dispatch('read', PathSpec.fromStrPath(target, undefined, '/'), [], {
                filetype: null,
                direct: true,
            }));
            base = stored;
        }
        catch (err) {
            if (!isMissingPath(err))
                throw err;
        }
        await this.op(() => this.files.write(target, concat([base, bytes])));
    }
    /** The read options for one view: the stored bytes, else the rendering. */
    static stored(raw) {
        return raw ? { raw } : undefined;
    }
    /** The key one view of a file's hydration goes by. */
    static view(key, raw) {
        return `${raw ? 'stored' : 'rendered'}:${key}`;
    }
    // ── POSIX surface (throws; adapters classify) ────────────────────
    /**
     * POSIX attributes for a path. One stat through the dispatcher answers: a
     * link the session cannot see is absent, as it is to the shell, and a
     * visible one reports its own row with its target read through the
     * dispatcher too. `follow` reports a trailing link's target rather than
     * the link (stat rather than lstat).
     */
    async getattr(path, follow = false, ctx = null) {
        if (path === '/')
            return this.rootAttrs();
        // macOS Finder/Spotlight probes .DS_Store, ._*, .Spotlight-V100, etc.
        // Reject early to avoid hitting the ops layer.
        if (isMacosMetadata(path.slice(path.lastIndexOf('/') + 1)))
            throw enoent(path);
        const s = await this.row(path, follow, ctx);
        if (isLink(s)) {
            const target = await this.op(() => this.files.readlink(this.resolve(path)));
            return this.attrs(s, new TextEncoder().encode(this.shownTarget(path, target)).byteLength);
        }
        return this.attrs(s);
    }
    /**
     * The row a stat of `path` answers, the size an open handle holds and what
     * it wrote and has not landed counted. Mirrors Python's `_row`.
     */
    async row(path, follow, ctx, reuse = false) {
        let size = ctx?.data?.byteLength ?? null;
        let s;
        let retained = false;
        if (ctx?.detached !== undefined) {
            // Its name is gone: it stats by the row its file had then.
            retained = true;
            s = ctx.detached;
        }
        else if (reuse &&
            ctx?.row !== undefined &&
            performance.now() - (ctx.rowAt ?? 0) < FSTAT_ROW_SECONDS * 1000) {
            // Judged as a stat now, as a held row is, without a fetch.
            retained = true;
            s = ctx.row;
        }
        else {
            const restats = ctx?.restats ?? 0;
            try {
                s = await this.op(() => this.files.stat(this.resolve(path), undefined, { nofollow: !follow }));
                if (ctx !== null && (ctx.restats ?? 0) === restats) {
                    ctx.row = s;
                    ctx.rowAt = performance.now();
                }
            }
            catch (err) {
                if (size === null || classify(err) !== 'ENOENT')
                    throw err;
                // A hide is not an unlink: held metadata still meets the walk.
                retained = true;
                s = new FileStat({ name: path.slice(path.lastIndexOf('/') + 1), type: FileType.FILE });
            }
        }
        if (isLink(s))
            return s;
        if (isDir(s))
            return retained ? this.statCached(path, s) : s;
        // A size-unknown file stats as 0 rather than be fetched on every ls -l;
        // direct_io keeps its reads whole regardless.
        if (size === null && s.size === null)
            size = this.heldSize(path);
        const writes = this.writesOn(ctx === null ? this.identity(path, follow) : ctx.key, ctx);
        if (writes.length > 0) {
            let end = size ?? contentSize(s);
            for (const [offset, data] of writes)
                end = Math.max(end, offset + data.byteLength);
            size = end;
        }
        if (size !== null && size !== s.size)
            s = s.with({ size });
        // A row the stat above fetched was judged there; a held one is judged
        // here, with the size the handle answers.
        return retained ? this.statCached(path, s) : s;
    }
    /**
     * Attributes through an open handle: the path's row, with the size the
     * handle holds, what it wrote and has not flushed included.
     */
    async fgetattr(path, fd) {
        const ctx = this.handles.get(fd) ?? null;
        if (ctx !== null)
            await this.settled(ctx);
        return this.getattr(ctx?.path ?? path, ctx !== null, ctx);
    }
    /**
     * The row a stat of `path` answers, what the handles open on it wrote and
     * have not landed counted, as a guest projects it into its own record.
     * Mirrors Python's `stat`.
     */
    async stat(path, follow = true) {
        return this.row(path, follow, null);
    }
    /**
     * The row an open handle stats by: its file's, with the size it holds and
     * what it wrote and has not landed counted, judged as a stat of its path.
     * A guest projects this into its own record. Mirrors Python's `fstat`.
     */
    async fstat(fd) {
        const ctx = this.openHandle(fd);
        await this.settled(ctx);
        return this.row(ctx.path, true, ctx, true);
    }
    /** The handle `fd` names; EBADF for any other number. */
    openHandle(fd) {
        const ctx = this.handles.get(fd);
        if (ctx === undefined)
            throw ebadf(String(fd));
        return ctx;
    }
    /**
     * Where the file a handle holds ends: its bytes, else the length its open
     * saw, and past them what it wrote.
     */
    static end(ctx) {
        let end = ctx.data?.byteLength ?? ctx.chunked?.size ?? ctx.size ?? 0;
        for (const [offset, data] of ctx.writeBuf ?? [])
            end = Math.max(end, offset + data.byteLength);
        return end;
    }
    /**
     * The mount root's attrs: its own row through the dispatcher, so a chmod
     * made on it shows, or a plain directory when nothing answers for it (a
     * workspace with no mount at `/`). Mirrors Python's `root_attrs`.
     */
    async rootAttrs() {
        let s;
        try {
            s = await this.op(() => this.files.stat(this.resolve('/')));
        }
        catch (err) {
            if (classify(err) !== 'ENOENT')
                throw err;
            console.debug(`fuse: the mount root has no row of its own: ${String(err)}`);
            s = await this.statCached('/', new FileStat({ name: '/', type: FileType.DIRECTORY }));
        }
        return this.attrs(s);
    }
    async readdir(path) {
        const names = new Set();
        const entries = await this.op(() => this.files.readdir(this.resolve(path)));
        for (const e of entries) {
            const part = rstripSlash(e).split('/').pop() ?? '';
            if (part !== '' && !isMacosMetadata(part))
                names.add(part);
        }
        return ['.', '..', ...[...names].sort(compareCodePoints)];
    }
    /**
     * A directory's entries with their attributes, in one pass. The entries
     * are stat'd together rather than one after another, with at most
     * `LISTING_CONCURRENCY` stats out at once across every listing. A stat
     * that fails ends the listing, and the ones not yet sent are not. An
     * entry that vanishes between the listing and its stat is left out, as
     * `ls` leaves out a file deleted mid-listing. `names` are the entries to
     * stat, in the order they are answered; null takes `readdir`'s, `.` and
     * `..` first. Mirrors Python's `listing`.
     */
    async listing(path, names = null, follow = false) {
        const listed = names ?? (await this.readdir(path));
        const rows = new Array(listed.length).fill(null);
        const todo = listed.entries();
        const refused = { failed: false };
        const worker = async () => {
            for (const [index, name] of todo) {
                const child = name === '.' ? path : name === '..' ? parent(path) : posixNormpath(`${path}/${name}`);
                const release = await STATS.acquire();
                try {
                    // A listing that failed stats nothing more, a worker that was
                    // waiting for a slot included.
                    if (refused.failed)
                        return;
                    rows[index] = [name, await this.getattr(child, follow)];
                }
                catch (err) {
                    if (!VANISHED.has(classify(err))) {
                        if (!refused.failed)
                            Object.assign(refused, { error: err, failed: true });
                        return;
                    }
                    console.debug(`${child} vanished while listing: ${String(err)}`);
                }
                finally {
                    release();
                }
            }
        };
        // Every worker settles before the listing answers, so none sends a stat
        // after it; the first refusal is the answer.
        await Promise.all(Array.from({ length: Math.min(LISTING_CONCURRENCY, listed.length) }, worker));
        if (refused.failed)
            throw refused.error;
        return rows.filter((row) => row !== null);
    }
    async read(path, fd, pos, len) {
        const ctx = this.handles.get(fd);
        if (ctx === undefined) {
            // Whole, as a handle's first read is: the read that fills the cache
            // and records the version a conditional write sends.
            const data = await this.op(() => this.files.read(this.resolve(path)));
            const writes = this.writesOn(this.identity(path), null);
            if (writes.length > 0) {
                const laid = await windowWrites(writes, pos, len);
                return this.readCached(path, overlaid(data.subarray(pos), pos, len, laid));
            }
            return data.subarray(pos, pos + len);
        }
        if (!ctx.mode.readable)
            throw ebadf(ctx.path);
        if (ctx.lost !== undefined && ctx.data === undefined)
            throw ctx.lost;
        await this.settled(ctx);
        // A handle that writes reads the stored bytes its writes land on; one
        // that only reads sees the rendering.
        const raw = ctx.mode.writable;
        let stored;
        let retained = false;
        if (ctx.live === true) {
            stored = await this.op(() => this.files.read(this.resolve(ctx.path), {
                offset: pos,
                size: len,
                ...MountCore.stored(raw),
            }));
        }
        else if (ctx.chunked !== undefined && ctx.data === undefined) {
            // A fetch joins what it brings onto bytes kept at `pos`, so any kept
            // byte there makes the answer held.
            retained = ctx.chunked.holds(pos, len) || ctx.chunked.peek(pos, 1).byteLength > 0;
            stored = await ctx.chunked.pread(pos, len);
        }
        else {
            let held = ctx.data;
            if (held === undefined) {
                // A change landing while this read is out keeps it from holding
                // what it fetched.
                const generation = ctx.generation ?? 0;
                held = await this.op(() => this.files.read(this.resolve(ctx.path), MountCore.stored(raw)));
                if ((ctx.generation ?? 0) === generation)
                    ctx.data = held;
            }
            else {
                retained = true;
            }
            stored = held.subarray(pos, pos + len);
        }
        const writes = this.writesOn(ctx.key, ctx);
        const data = writes.length > 0 ? overlaid(stored, pos, len, await windowWrites(writes, pos, len)) : stored;
        return retained || writes.length > 0 ? this.readCached(ctx.path, data) : data;
    }
    /**
     * What a read of the file `key` names sees that has not landed: every
     * handle's writes on it, as write(2) is seen by every reader at once.
     * `supersede` keeps them apart, so they lay over the stored bytes in any
     * order; an appending handle's are its own, since they land at the end the
     * file has then, and the reader's own go last. Mirrors Python's
     * `_writes_on`.
     */
    writesOn(key, ctx) {
        const out = [];
        for (const other of this.openOn(key)) {
            if (other !== ctx && !other.mode.append)
                for (const item of other.writeBuf ?? [])
                    out.push(item);
        }
        if (ctx !== null)
            for (const item of ctx.writeBuf ?? [])
                out.push(item);
        return out;
    }
    /**
     * Buffer a write on its handle, or apply it directly when there is none.
     * An appending handle writes at the end it holds, whatever `pos` says;
     * EBADF through a handle opened only to read.
     */
    async write(path, fd, data, pos) {
        const ctx = this.handles.get(fd);
        if (ctx !== undefined) {
            if (!ctx.mode.writable)
                throw ebadf(ctx.path);
            const at = ctx.mode.append ? MountCore.end(ctx) : pos;
            this.supersede(ctx.key, at, data.byteLength, ctx);
            await MountCore.buffer(ctx, at, data);
            return;
        }
        const key = this.identity(path);
        this.supersede(key, pos, data.byteLength, undefined);
        await this.mutate(key, () => this.applyWrites(path, [[pos, data]]));
    }
    /**
     * Keep a write on its handle, a copy: one that starts inside the last run
     * or right at its end folds into it, so a stream of writes stays one run;
     * any other starts a run of its own, laid over the earlier ones in order,
     * and every 64 of those fold together while none waits in the spool. Past
     * `MEMORY_LIMIT` held in memory, the handle's runs move to its spool, and
     * a stream goes on at the spool's end. A spool append is queued as it is
     * called, ahead of any read of the window it publishes. Mirrors Python's
     * `_buffer`.
     */
    static async buffer(ctx, pos, data) {
        if (data.byteLength === 0)
            return;
        ctx.writeBuf ??= [];
        const last = ctx.writeBuf.at(-1);
        if (last !== undefined) {
            const [start, run] = last;
            const end = start + run.byteLength;
            if (run instanceof Uint8Array && start <= pos && pos <= end) {
                last[1] = MountCore.folded(run, pos - start, data);
                await MountCore.spill(ctx);
                return;
            }
            if (run instanceof ByteSlice &&
                pos === end &&
                run.source === ctx.spool &&
                run.offset + run.size === ctx.spooled) {
                last[1] = new ByteSlice(run.source, run.offset, run.size + data.byteLength);
                ctx.spooled += data.byteLength;
                await run.source.append(data);
                return;
            }
        }
        ctx.writeBuf.push([pos, new Uint8Array(data)]);
        if (ctx.writeBuf.length >= 64) {
            const held = MountCore.held(ctx.writeBuf);
            if (held.length === ctx.writeBuf.length)
                ctx.writeBuf = writeRuns(held);
        }
        await MountCore.spill(ctx);
    }
    /** The buffered writes held in memory, in arrival order. */
    static held(writes) {
        return writes.filter((write) => write[1] instanceof Uint8Array);
    }
    /**
     * Move a handle's runs held in memory to its spool once they hold more
     * than `MEMORY_LIMIT`. Each takes its place in the spool before the spool
     * has its bytes, in one append, so a write or a read meanwhile finds the
     * run where it will be. Mirrors Python's `_spill`.
     */
    static async spill(ctx) {
        const writes = ctx.writeBuf ?? [];
        const held = [];
        let size = 0;
        for (const [, run] of writes) {
            if (run instanceof Uint8Array) {
                held.push(run);
                size += run.byteLength;
            }
        }
        if (size <= MEMORY_LIMIT)
            return;
        if (ctx.spool === undefined) {
            ctx.spool = new ByteSpool();
            ctx.spooled = 0;
        }
        const spool = ctx.spool;
        for (const write of writes) {
            const run = write[1];
            if (!(run instanceof Uint8Array))
                continue;
            write[1] = new ByteSlice(spool, ctx.spooled ?? 0, run.byteLength);
            ctx.spooled = (ctx.spooled ?? 0) + run.byteLength;
        }
        await spool.append(concat(held));
    }
    /**
     * `run` with `data` laid at `at`, inside it or right at its end: in place
     * when the run is one this grew and its buffer has the room, else in a
     * buffer twice as long, so a stream copies what it holds only as often as
     * its length doubles, as Python's bytearray grows.
     */
    static folded(run, at, data) {
        const length = Math.max(run.byteLength, at + data.byteLength);
        let out;
        if (MountCore.grown.has(run) && run.byteOffset + length <= run.buffer.byteLength) {
            out = new Uint8Array(run.buffer, run.byteOffset, length);
            MountCore.grown.delete(run);
        }
        else {
            out = new Uint8Array(Math.max(length, 2 * run.byteLength)).subarray(0, length);
            out.set(run);
        }
        out.set(data, at);
        MountCore.grown.add(out);
        return out;
    }
    /**
     * Cut a new write's range out of what the file's other handles hold:
     * their bytes there are older, and the new write replaces them. No two
     * handles on a file then hold the same bytes, so whichever flush, release
     * or sync lands first, the file ends as its writes were made. Nothing
     * lands here, so a write never waits on one.
     */
    supersede(key, pos, size, ctx) {
        // Its bytes land at the end the file has then, not here.
        if (ctx?.mode.append === true)
            return;
        for (const other of this.openOn(key)) {
            if (other === ctx || other.mode.append || other.writeBuf === undefined)
                continue;
            other.writeBuf = MountCore.cut(other.writeBuf, pos, size);
            other.cuts?.push([pos, size]);
        }
    }
    /** `writes` with the range a newer write took cut out of them. Mirrors Python's `_cut`. */
    static cut(writes, pos, size) {
        const end = pos + size;
        const kept = [];
        for (const [at, run] of writes) {
            if (at >= end || at + run.byteLength <= pos) {
                kept.push([at, run]);
                continue;
            }
            if (at < pos)
                kept.push([at, run.subarray(0, pos - at)]);
            if (at + run.byteLength > end)
                kept.push([end, run.subarray(end - at)]);
        }
        return kept;
    }
    /**
     * Put a failed landing's writes back ahead of those made since, less what
     * other writes took meanwhile, so a retry never lands bytes older than
     * theirs. Mirrors Python's `_put_back`.
     */
    static putBack(ctx, writes) {
        let kept = writes;
        for (const [pos, size] of ctx.cuts ?? [])
            kept = MountCore.cut(kept, pos, size);
        ctx.writeBuf = [...kept, ...(ctx.writeBuf ?? [])];
    }
    /**
     * Create an empty file and return a fresh handle. `mode` is what the
     * creator asked for, umask applied; null takes the mount's default.
     */
    async create(path, mode = null) {
        const key = this.identity(path);
        await this.make(path, mode);
        return this.handles.add({ path, key, mode: { ...READ, writable: true, create: true } });
    }
    /** Create an empty file in the file's turn. */
    async make(path, mode) {
        await this.mutate(this.identity(path), async () => {
            await this.op(() => this.files.create(this.resolve(path)));
            await this.keepMode(path, mode);
            await this.changed(path);
        });
    }
    /** Create a directory, keeping the mode it was asked for. */
    async mkdir(path, mode = null) {
        await this.op(() => this.files.mkdir(this.resolve(path)));
        await this.keepMode(path, mode);
    }
    /**
     * Store the mode a create asked for, when it is not the one the backend
     * made the entry with (a disk mount applies the daemon's umask), so
     * `open(O_CREAT, 0600)` and `mkdir -m` read back as asked without a
     * write per ordinary create. Mirrors Python's `_keep_mode`.
     */
    async keepMode(path, mode) {
        if (mode === null)
            return;
        try {
            const made = await this.op(() => this.files.stat(this.resolve(path)));
            if ((posixMode(made) & 0o7777) !== (mode & 0o7777))
                await this.setattr(path, mode);
        }
        catch (err) {
            // The entry exists: a policy that refuses the stat or the chmod leaves
            // the mode it was made with rather than failing a create that landed.
            console.debug(`mount: keeping the mode of ${path} failed: ${String(err)}`);
        }
    }
    /** The target of a namespace link, read through the dispatcher; EINVAL when not a link. */
    async readlink(path) {
        const target = await this.op(() => this.files.readlink(this.resolve(path)));
        return this.shownTarget(path, target);
    }
    /**
     * Create namespace link `dest -> src` (ln -s src dest; libfuse passes
     * the pointee first). Relative sources are stored verbatim (resolved
     * at follow time, exactly like the shell `ln -s`); absolute sources
     * are mapped into virtual space so a scoped mount stores the path it
     * will later follow.
     */
    async symlink(src, dest) {
        // The write routes through the dispatcher like every other FUSE op, so
        // session grants and admission policies refuse a scoped kernel
        // mount exactly like a scoped shell.
        const stored = src.startsWith('/') ? this.resolve(src) : src;
        await this.op(() => this.files.symlink(this.resolve(dest), stored));
    }
    /**
     * Remove the entry at `path`, a link entry like any other.
     *
     * A link routes through the dispatcher rather than straight to the node
     * table: `unlink` is a LINK_ENTRY_OPS member, so the dispatcher answers a
     * link path itself, gated by session grants and admission policies
     * and recorded on the ledger. Writing the table here instead let a
     * session-scoped kernel mount delete a link on a mount its profile
     * hides. Mirrors Python's MountCore.unlink.
     */
    async unlink(path) {
        const key = this.identity(path, false);
        await this.removing([path], () => this.mutate(key, async () => {
            const named = this.named(path);
            const row = await this.hold(path, named);
            await this.op(() => this.files.unlink(this.resolve(path)));
            this.detach(named, row);
            await this.changed(path, false);
        }));
    }
    /**
     * The one function every mutation of a file's bytes goes through. The
     * open handles on a file are matched by its identity (the mount path with
     * namespace links followed), and this is the only place their bytes are
     * refreshed, so a new mutating op cannot forget one of them and a link
     * alias cannot slip past. A hydration still in flight is outdated so it
     * re-reads instead of handing over what it fetched, and hydrated handles
     * on the file are refreshed in one read through the dispatcher, so fstat
     * and read through any of them, including the handle that wrote, see the
     * new bytes. A removal or rename passes `rehydrate = false`: POSIX keeps
     * an open descriptor on the bytes it had, and the name, not what it
     * points at, is what changed. A refresh that fails is logged
     * and leaves the handles unhydrated rather than failing the committed
     * mutation. Mirrors Python's `_changed`.
     */
    async changed(path, rehydrate = true) {
        const key = this.identity(path, rehydrate);
        this.restatKey(key);
        for (const view of [MountCore.view(key, true), MountCore.view(key, false)]) {
            if (this.hydrations.has(view)) {
                this.hydrationGen.set(view, (this.hydrationGen.get(view) ?? 0) + 1);
            }
        }
        if (!rehydrate)
            return;
        const handles = this.openOn(key);
        for (const ctx of handles) {
            ctx.generation = (ctx.generation ?? 0) + 1;
            ctx.chunked?.drop();
        }
        for (const raw of [true, false]) {
            const hydrated = handles.filter((ctx) => ctx.data !== undefined && ctx.mode.writable === raw);
            if (hydrated.length === 0)
                continue;
            let data;
            try {
                data = await this.op(() => this.files.read(this.resolve(path), MountCore.stored(raw)));
            }
            catch (err) {
                // The mutation landed, so a refresh that fails must not fail it; the
                // next read through these handles fetches and reports.
                console.warn(`fuse: refresh of ${path} after a change failed: ${String(err)}`);
                for (const ctx of hydrated)
                    delete ctx.data;
                continue;
            }
            for (const ctx of hydrated)
                ctx.data = data;
        }
        // An appending handle holding no bytes writes at the length its open
        // saw; the file has another one now.
        const sized = handles.filter((ctx) => ctx.mode.append && ctx.data === undefined);
        if (sized.length === 0)
            return;
        try {
            const s = await this.op(() => this.files.stat(this.resolve(path)));
            for (const ctx of sized) {
                ctx.size = contentSize(s);
                if (ctx.chunked !== undefined)
                    ctx.chunked.size = contentSize(s);
            }
        }
        catch (err) {
            console.warn(`fuse: the length of ${path} after a change failed: ${String(err)}`);
        }
    }
    /**
     * Resize the file an open handle is on: through its name, or, on a file
     * whose name went, the bytes its handles hold. EBADF through a handle
     * opened only to read, EINVAL for a negative length. Mirrors Python's
     * `ftruncate`.
     */
    async ftruncate(fd, length) {
        const ctx = this.openHandle(fd);
        if (!ctx.mode.writable)
            throw ebadf(ctx.path);
        if (length < 0)
            throw einval(ctx.path);
        const row = ctx.detached;
        if (row === undefined) {
            await this.truncate(ctx.path, length);
            return;
        }
        // In the file's turn, as a flush lands, so a read waits for the
        // writes this lands first.
        await this.mutate(ctx.key, async () => {
            if (ctx.writeBuf !== undefined && ctx.writeBuf.length > 0)
                await this.landHeld(ctx, ctx.detached ?? row);
            const held = ctx.data ?? new Uint8Array();
            const data = new Uint8Array(length);
            data.set(held.subarray(0, length));
            const landed = (ctx.detached ?? row).with({ size: length });
            for (const other of this.openOn(ctx.key)) {
                if (other !== ctx && other.data !== held)
                    continue;
                other.data = data;
                other.detached = landed;
                other.writeBuf = (other.writeBuf ?? [])
                    .filter(([at]) => at < length)
                    .map(([at, run]) => [at, run.subarray(0, length - at)]);
            }
        });
    }
    async rename(src, dst) {
        // The facade is where a cross-mount pair is refused with EXDEV,
        // which is what makes `mv` between two backends fall back to
        // copy+unlink instead of addressing the destination against the
        // source's backend.
        const source = this.resolve(src);
        const target = this.resolve(dst);
        const moved = this.identity(src, false);
        await this.removing([src, dst], () => this.queueAll(this.pending, [moved, this.identity(dst, false)], async () => {
            const replaced = this.named(dst).filter((ctx) => ctx.key !== moved);
            const row = await this.hold(dst, replaced);
            // Both ends as the links above them lead in this turn, right before
            // the move: a link the move takes along no longer leads there after.
            const start = this.identity(src, false);
            const landing = this.identity(dst, false);
            await this.op(() => this.files.rename(source, target));
            this.detach(replaced, row);
            for (const ctx of this.handles.values()) {
                if (ctx.detached !== undefined)
                    continue;
                if (ctx.key === start || ctx.key.startsWith(`${start}/`)) {
                    const rest = ctx.key.slice(start.length);
                    ctx.key = landing + rest;
                    ctx.path = this.mountPath(ctx.key, dst + rest);
                }
            }
            await this.changed(src, false);
            await this.changed(dst, false);
        }));
    }
    // No emptiness pre-check: every backend's rmdir refuses a non-empty
    // directory itself.
    async rmdir(path) {
        await this.op(() => this.files.rmdir(this.resolve(path)));
    }
    /**
     * The mount path the file `key` names, or `spelled` where it lies outside
     * the mount's root.
     */
    mountPath(key, spelled) {
        return key.startsWith(`${this.root}/`) ? key.slice(this.root.length) : spelled;
    }
    /**
     * Where a mount path really points: the mount-resolved path with every
     * namespace link followed, so two handles opened through a link and
     * its target are recognised as the same file.
     */
    identity(path, follow = true) {
        const virtual = this.resolve(path);
        const reader = readerOf(this.files);
        // An op on the entry itself (unlink, rename, lsetattr) names the link,
        // which may loop: only the links above it are followed.
        const resolved = () => follow ? posixNormpath(reader.follow(virtual)) : reader.followParent(virtual);
        // Judged as the bound session judges every op (`op`): outside it the
        // facade's session is the default one, whose profile may hide a link
        // this session's handles were opened through.
        if (this.session === null)
            return resolved();
        return callsAs({ ...currentCaller(), session: this.session }, resolved)();
    }
    /**
     * Persist a handle's buffered writes. The buffer is detached before the
     * await so a write arriving meanwhile is not lost to the clear, and the
     * runs that did not land are restored ahead of those later writes when
     * persistence fails, so the acknowledged bytes stay for the handle's own
     * flush to retry.
     */
    async settle(ctx) {
        // Queued with nothing buffered too: a flush still landing has taken the
        // buffer, and this one waits for it and retries what it puts back,
        // rather than reporting the file settled.
        for (;;) {
            const key = ctx.key;
            const landed = await this.mutate(key, async () => {
                // A rename that moved the handle while this waited put its file in
                // another queue: it lands from that one instead.
                if (ctx.key !== key)
                    return false;
                await this.persistBuffered(ctx);
                return true;
            });
            if (landed)
                return;
        }
    }
    /** The handles open on the entry at `path` itself. */
    named(path) {
        return this.openOn(this.identity(path, false));
    }
    /**
     * The handles open on the file `key` names. The handles left on a file
     * whose name went share a key of their own (`detach`), so no path names
     * them.
     */
    openOn(key) {
        return [...this.handles.values()].filter((ctx) => ctx.key === key);
    }
    /**
     * Leave the handles open on a name that went on the file it had: they
     * stat by its row from then on, and share a key no path gives, so a write
     * through one reaches the others and nothing that opens the name later.
     */
    detach(named, row) {
        this.removed += 1;
        const key = `\0${String(this.removed)}`;
        for (const ctx of named) {
            ctx.detached = row;
            ctx.key = key;
        }
    }
    /**
     * Land a handle's writes on a file whose name went: on the bytes it holds,
     * which every handle open on that file reads, as writes to an unlinked
     * file stay with it. A handle reading a rendering the stored bytes differ
     * from keeps its own. The writes leave the handle before its spool is read,
     * an empty run at their end in their place, so a write arriving meanwhile
     * stays for the next landing and an append starts past them; the spool
     * stays the handle's, closed once none of its writes wait there. Mirrors
     * Python's `_land_held`.
     */
    async landHeld(ctx, row) {
        const held = ctx.data ?? new Uint8Array();
        const end = MountCore.end(ctx);
        const writes = ctx.writeBuf ?? [];
        const marker = new Uint8Array();
        ctx.writeBuf = [[end, marker]];
        ctx.cuts = [];
        let data;
        try {
            data = overlaid(held, 0, end, await windowWrites(writes, 0, end));
        }
        catch (err) {
            MountCore.putBack(ctx, writes);
            throw err;
        }
        finally {
            delete ctx.cuts;
            ctx.writeBuf = ctx.writeBuf.filter(([, run]) => run !== marker);
        }
        await MountCore.freeSpool(ctx);
        const landed = row.with({ size: data.byteLength });
        for (const other of this.openOn(ctx.key)) {
            if (other !== ctx && other.data !== held)
                continue;
            other.data = data;
            other.detached = landed;
        }
    }
    /**
     * Land what every handle open on `path` wrote, in the file's turn, as a
     * change of its times by path must come after; `follow` says the change
     * follows a link at `path`. Mirrors Python's `land`.
     */
    async land(path, follow = true) {
        const key = this.identity(path, follow);
        await this.mutate(key, () => this.landBuffered(key));
    }
    /**
     * Have the handles open on `path` stat it afresh, after a change by path
     * made past the core (a runtime's setattr or whole-file write), so their
     * next fstat agrees with stat; `follow` says the change followed a link
     * at `path`. Mirrors Python's `restat`.
     */
    restat(path, follow = true) {
        this.restatKey(this.identity(path, follow));
    }
    /**
     * Drop the stat rows the handles open on `key` keep, and outdate a stat
     * of theirs still out. Mirrors Python's `_restat`.
     */
    restatKey(key) {
        for (const ctx of this.openOn(key)) {
            delete ctx.row;
            ctx.restats = (ctx.restats ?? 0) + 1;
        }
    }
    /** Land the buffered writes of every handle open on `key`. */
    async landBuffered(key) {
        for (const ctx of this.openOn(key))
            await this.persistBuffered(ctx);
    }
    async persistBuffered(ctx) {
        if (ctx.writeBuf === undefined || ctx.writeBuf.length === 0)
            return;
        if (ctx.detached !== undefined) {
            await this.landHeld(ctx, ctx.detached);
            return;
        }
        const held = MountCore.held(ctx.writeBuf);
        if (held.length < ctx.writeBuf.length) {
            const groups = runGroups(ctx.writeBuf);
            ctx.writeBuf = [];
            ctx.cuts = [];
            try {
                await this.landSpooled(ctx, groups);
            }
            catch (err) {
                MountCore.putBack(ctx, groups.flat());
                throw err;
            }
            finally {
                delete ctx.cuts;
            }
            await MountCore.freeSpool(ctx);
            return;
        }
        const runs = writeRuns(held);
        ctx.writeBuf = [];
        ctx.cuts = [];
        try {
            await this.applyWrites(ctx.path, runs, ctx.mode.append);
        }
        catch (err) {
            MountCore.putBack(ctx, runs);
            throw err;
        }
        finally {
            delete ctx.cuts;
        }
    }
    /**
     * Land a buffer whose writes wait in its spool, each group of writes
     * `writeRuns` would fold into one run as a stream, in order. A group from
     * the file's start that reaches its stored end leaves nothing of what is
     * stored, so it lands as a write of the whole file, which a store with no
     * pwrite (S3) takes in parts rather than reading the file back first; any
     * other lands as one streamed pwrite, and an appending handle's as
     * appends. The groups that landed leave `groups`, as `applyWrites` drops
     * its runs. Mirrors Python's `_land_spooled`.
     */
    async landSpooled(ctx, groups) {
        const target = this.resolve(ctx.path);
        let landed = 0;
        try {
            for (const group of groups) {
                const start = group[0]?.[0] ?? 0;
                let end = start;
                for (const [at, run] of group)
                    end = Math.max(end, at + run.byteLength);
                if (ctx.mode.append)
                    await this.appendTo(target, group);
                else if (start === 0 && (await this.endsBy(ctx.path, end))) {
                    // TODO(MIRAGE-34): hand a whole spool over, not a second copy.
                    await this.op(() => this.files.write(target, runStream(group)));
                }
                else {
                    await this.op(() => this.files.pwrite(target, runStream(group), start));
                }
                landed += 1;
            }
        }
        finally {
            groups.splice(0, landed);
            await this.changed(ctx.path);
        }
    }
    /**
     * Whether the stored file at `path` is missing or ends at or before `end`,
     * by a fresh stat, which waits out a write already landing on the path, so
     * an append under way is counted. A stat refused (a policy may allow the
     * write and not it) answers no, so the group lands as a pwrite rather than
     * not at all. Mirrors Python's `_ends_by`.
     */
    async endsBy(path, end) {
        let row;
        try {
            row = await this.op(() => this.files.stat(this.resolve(path), undefined, { fresh: true }));
        }
        catch (err) {
            const code = classify(err);
            if (code === 'ENOENT')
                return true;
            if (code !== 'EACCES')
                throw err;
            console.debug(`fuse: ${path} lands as a pwrite, unsized: ${String(err)}`);
            return false;
        }
        return row.size !== null && row.size <= end;
    }
    /** Close a handle's spool once none of its writes wait there. Mirrors Python's `_free_spool`. */
    static async freeSpool(ctx) {
        const spool = ctx.spool;
        if (spool === undefined || (ctx.writeBuf ?? []).some(([, run]) => run instanceof ByteSlice))
            return;
        delete ctx.spool;
        await spool.close();
    }
    async truncate(path, size) {
        // A write the kernel already acknowledged on another handle precedes
        // this truncation in POSIX order, so it is flushed first rather than
        // left queued to land over the shortened file at that handle's
        // release. Handles are matched by identity, not by the path they
        // were opened through, so a link alias is settled too. Mirrors
        // Python's MountCore.truncate.
        const key = this.identity(path);
        await this.mutate(key, async () => {
            await this.landBuffered(key);
            await this.op(() => this.files.truncate(this.resolve(path), size));
            await this.changed(path);
        });
    }
    statfs() {
        return {
            bsize: 4096,
            frsize: 4096,
            blocks: 1024 * 1024,
            bfree: 1024 * 1024,
            bavail: 1024 * 1024,
            files: 1_000_000,
            ffree: 1_000_000,
            favail: 1_000_000,
            namemax: 255,
        };
    }
    /**
     * Store metadata through the dispatcher. The backend keeps what it can and
     * the namespace overlay the rest, so a chmod, chown or `touch -d` through
     * the mount is what `stat` in a shell reads back, on a backend with no
     * permission bits or settable times of its own too. A null field is left
     * as it is. Mirrors Python's `MountCore.setattr`.
     */
    async setattr(path, mode, uid = null, gid = null, atime = null, mtime = null, follow = false) {
        // The kernel has already resolved any link the call follows, so a
        // kernel mount names the entry to change, a link itself for `chown -h`;
        // SFTP's setstat asks to follow one.
        const fields = { nofollow: !follow };
        if (mode !== null)
            fields.mode = mode & 0o7777;
        if (uid !== null)
            fields.uid = uid;
        if (gid !== null)
            fields.gid = gid;
        if (atime !== null)
            fields.atime = atime.toISOString();
        if (mtime !== null)
            fields.mtime = mtime.toISOString();
        const key = this.identity(path, follow);
        const timed = atime !== null || mtime !== null;
        const run = async () => {
            // cp -p sets times on a file it still holds open: its buffered
            // writes land first, or they would stamp over them.
            if (timed)
                await this.landBuffered(key);
            await this.op(() => this.files.setattr(this.resolve(path), fields));
            this.restatKey(key);
        };
        await (timed ? this.mutate(key, run) : run());
    }
    /**
     * Store an extended attribute through the dispatcher, which keeps
     * it on the path's namespace node: it outlives the mount, moves with a
     * rename, and is the attribute every other surface (the shell's
     * getfattr, a guest's os.getxattr) reads. Tools that set xattrs as a
     * matter of course (rsync -aX, tar --xattrs, cp -p, Finder writing
     * com.apple.*) succeed on a backend with no attribute slot of its own.
     * Mirrors the python MountCore.
     */
    async setxattr(path, name, value, opts = {}) {
        await this.op(() => this.files.setxattr(this.resolve(path), name, Uint8Array.from(value), opts));
    }
    /** One attribute, the backend's own facts included; ENODATA when unset. */
    async getxattr(path, name) {
        return this.op(() => this.files.getxattr(this.resolve(path), name));
    }
    async listxattr(path) {
        return this.op(() => this.files.listxattr(this.resolve(path)));
    }
    async removexattr(path, name) {
        await this.op(() => this.files.removexattr(this.resolve(path), name));
    }
    /**
     * Open a path, the open's effect landing before any byte moves. One rule
     * for every entry point, however it spells the open (open(2) flags, SFTP
     * pflags, an fopen mode, preview1 oflags): an exclusive create refuses
     * what exists, a dangling link or a listed directory included; a
     * directory refuses a mode that would write it; a missing path is created
     * when the mode creates and refused when it does not; a truncating mode
     * empties what exists, which CPython's `open('w')` leaves behind even when
     * nothing is written; and a mode that writes is admitted against the
     * file's write rules before its first write. A file an API mount cannot
     * size is then read whole. `perm` is the mode bits a create gives the
     * file, umask applied; null takes the mount's default. Mirrors Python's
     * `MountCore.open`.
     */
    async open(path, mode = READ, perm = null) {
        for (let removal = this.removalOf(path); removal !== undefined; removal = this.removalOf(path)) {
            await removal;
        }
        const keys = new Set([this.identity(path, false), this.identity(path)]);
        const opened = this.openPath(path, mode, perm);
        const out = opened.then(() => undefined, () => undefined);
        for (const key of keys)
            this.opening.set(key, (this.opening.get(key) ?? new Set()).add(out));
        try {
            return await opened;
        }
        finally {
            for (const key of keys) {
                const opening = this.opening.get(key);
                opening?.delete(out);
                if (opening?.size === 0)
                    this.opening.delete(key);
            }
        }
    }
    /** The open itself, which a removal of its name waits for. */
    async openPath(path, mode, perm) {
        if (path.endsWith('/') && path !== '/') {
            const leaf = await this.rowOrNull(rstripSlash(path));
            if (leaf !== null && !isDir(leaf))
                throw enotdir(path);
            if (leaf === null && mode.create)
                throw eisdir(path);
        }
        // An exclusive create follows no link (O_CREAT|O_EXCL), so a dangling
        // one is a name that is there. A path with no row may still be a
        // directory the mount lists, and a create there would put a file at a
        // directory's name.
        const s = await this.rowOrNull(path, mode.exclusive);
        const listed = s !== null ? isDir(s) : await this.listed(path);
        if (mode.exclusive && (s !== null || listed))
            throw eexist(path);
        const key = this.identity(path);
        if (listed) {
            if (mode.writable || mode.create || mode.directory !== true)
                throw eisdir(path);
            return this.handles.add({ path, key, mode });
        }
        // The handle reads and writes the file the open reached, not the one a
        // link at `path` names later.
        const at = this.mountPath(key, path);
        const emptied = s === null || mode.truncate;
        if (s === null) {
            if (!mode.create)
                throw enoent(path);
            await this.make(path, perm);
        }
        else if (mode.truncate) {
            await this.empty(path);
        }
        else if (mode.writable) {
            await this.op(() => this.files.dispatch('write', PathSpec.fromStrPath(this.resolve(path), undefined, '/'), [], {
                admit_only: true,
            }));
        }
        const ctx = {
            path: at,
            key,
            mode,
            live: s?.extra[LIVE_KEY] === true,
            size: s === null || mode.truncate ? 0 : contentSize(s),
        };
        if (ctx.live === true || s === null)
            return this.handles.add(ctx);
        if (!emptied) {
            ctx.row = s;
            ctx.rowAt = performance.now();
        }
        if (s.size === null) {
            // Hydrate through the rendered read path, after an O_TRUNC too: an
            // extension whose renderer gives an empty file a body is honored
            // rather than shadowed by literal raw emptiness.
            const data = await this.hydrate(at, mode.writable);
            if (data !== undefined)
                ctx.data = data;
        }
        else if (s.size > READ_CHUNK && !emptied) {
            // Read a chunk at a time, at the handle's path then (a rename moves
            // it): a `head` must not move the file.
            ctx.chunked = new ChunkedHandle(at, s.size, (offset, size) => this.op(() => this.files.read(this.resolve(ctx.path), {
                offset,
                size,
                ...MountCore.stored(mode.writable),
            })));
        }
        return this.handles.add(ctx);
    }
    /** The path's row, or null when the mount says it is not there. */
    async rowOrNull(path, nofollow = false) {
        try {
            return await this.op(() => this.files.stat(this.resolve(path), undefined, { nofollow }));
        }
        catch (err) {
            if (classify(err) !== 'ENOENT')
                throw err;
            return null;
        }
    }
    /**
     * Whether a path with no row is a directory the mount only implies (the
     * root above a nested mount), which it lists.
     */
    async listed(path) {
        try {
            await this.op(() => this.files.readdir(this.resolve(path)));
        }
        catch (err) {
            const code = classify(err);
            if (code === 'ENOENT' || code === 'ENOTDIR')
                return false;
            throw err;
        }
        return true;
    }
    /**
     * Empty a file an open truncates. A mount with no truncate (hf buckets,
     * databricks volumes) still empties it through an empty create, which is
     * the effect the open asked for.
     */
    async empty(path) {
        try {
            // libfuse 3 sends no SETATTR ahead of an O_TRUNC open (atomic
            // O_TRUNC), so the flag is the whole truncation.
            await this.truncate(path, 0);
        }
        catch (err) {
            if (classify(err) !== 'ENOTSUP')
                throw err;
            await this.make(path, null);
        }
    }
    /**
     * A removal of `path` still running. One is keyed by the name it takes, so
     * an open through a link meets the link's removal and then its target's.
     */
    removalOf(path) {
        return this.removals.get(this.identity(path, false)) ?? this.removals.get(this.identity(path));
    }
    /**
     * Keep what the handles open on `path` need once it goes, and return the
     * row they stat by from then on. POSIX keeps an open descriptor on the
     * file it had, so an unlink or a rename onto it must not leave a handle
     * reading or stat'ing the file at that name next. A handle holding no
     * bytes yet (never read, or chunked and holding one chunk) gets them all
     * from one read every such handle shares; it runs under `removing` and
     * the file's `pending` chain, so no handle opens on the file and no write
     * lands on it meanwhile. The handles are matched by the
     * entry itself, so removing a link holds nothing: it takes the link,
     * never its target's bytes. A stat or a read that fails (a policy may
     * allow the removal and refuse either) leaves a bare row or the handles
     * as they are, each on its own, rather than refusing a mutation the
     * caller is allowed. Mirrors Python's `MountCore._hold`.
     */
    async hold(path, named) {
        const bare = new FileStat({ name: path.slice(path.lastIndexOf('/') + 1), type: FileType.FILE });
        if (named.length === 0)
            return bare;
        const virtual = this.resolve(path);
        let row = bare;
        try {
            row = await this.op(() => this.files.stat(virtual));
        }
        catch (err) {
            console.warn(`fuse: the row of ${path} before it goes failed: ${String(err)}`);
        }
        for (const raw of [true, false]) {
            const lacking = named.filter((ctx) => ctx.data === undefined && ctx.live !== true && ctx.mode.writable === raw);
            if (lacking.length === 0)
                continue;
            let data;
            try {
                data = await this.op(() => this.files.read(virtual, MountCore.stored(raw)));
            }
            catch (err) {
                console.warn(`fuse: holding ${path} before it goes failed: ${String(err)}`);
                for (const ctx of lacking)
                    ctx.lost = err instanceof Error ? err : new Error(String(err));
                continue;
            }
            for (const ctx of lacking) {
                ctx.data = data;
                delete ctx.chunked;
            }
        }
        const copies = [];
        for (const ctx of named) {
            if (ctx.data === undefined)
                continue;
            const held = ctx.data;
            const same = copies.find((copy) => copy.byteLength === held.byteLength && findBytes(copy, held) === 0);
            if (same === undefined)
                copies.push(held);
            else
                ctx.data = same;
        }
        return row;
    }
    /**
     * Wait for a flush or truncation of the handle's file still landing, into
     * the queue a rename moved the handle to. Mirrors Python's `_settled`.
     */
    async settled(ctx) {
        let key = null;
        while (key !== ctx.key) {
            key = ctx.key;
            await this.pending.get(key);
        }
    }
    /**
     * Land a handle's buffered writes and let it go. The handle goes even
     * when the landing fails, as close(2) releases its descriptor: no
     * adapter retries a release, so a kept handle would never close. The
     * failure still reaches the caller.
     */
    async release(fd) {
        const ctx = this.handles.get(fd);
        try {
            // The FSKit shim sends RELEASE with no FLUSH before it.
            if (ctx !== undefined)
                await this.flush(ctx.path, fd);
        }
        finally {
            this.handles.pop(fd);
            if (ctx?.spool !== undefined)
                await ctx.spool.close();
        }
    }
    async flush(_path, fd) {
        const ctx = this.handles.get(fd);
        if (ctx !== undefined)
            await this.settle(ctx);
    }
    /**
     * Land what every open handle holds, as sync(2) does, for a client whose
     * close(2) returns before it releases the handle (sshfs). A landing that
     * fails is logged, and its bytes stay on the handle for its own flush to
     * retry and report.
     */
    async sync() {
        for (const ctx of [...this.handles.values()]) {
            try {
                await this.settle(ctx);
            }
            catch (err) {
                console.warn(`fuse: landing ${ctx.path} failed: ${String(err)}`);
            }
        }
    }
}
//# sourceMappingURL=core.js.map