@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
144 lines • 6.88 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 { ancestors } from "../../utils/path.js";
import { edquot, eexist, einval, eisdir, enoent, enotdir, enotempty } from "../../errors/fs.js";
import {} from "../../errors/types.js";
import { memoryRoom } from "../../io/memory.js";
import { mountedPath } from "../../utils/key_prefix.js";
import { rstripSlash } from "../../utils/slash.js";
// Reject a destination whose parent chain is not all directories. Mirrors how
// rename(2) resolves the destination: a component that does not exist is
// ENOENT, a component that is a plain file is ENOTDIR (at any depth). Without
// this the store grows a key under a directory it never recorded, and that
// orphan makes both the phantom directory and its real parent unlistable.
// Shared by every op that places a key at a caller-supplied path (rename,
// copy, write, append, mkdir; create routes through write): none of them
// creates parents (that is `mkdir -p`), so all owe the destination the same
// probe. The real-filesystem backends get this from the kernel.
export function checkDestParents(accessor, dst, d) {
const broken = brokenParent(accessor, dst, d);
if (broken !== null)
throw broken;
}
// Refuse a rename that rename(2) refuses once both ends resolve. A file onto
// a directory is EISDIR, a directory onto a file ENOTDIR, onto a directory
// that holds entries ENOTEMPTY, and into its own subtree EINVAL. A keyed store
// has no kernel to say so: without this a file and a directory end up under
// one key, or two trees merge. A source the store does not hold is left to the
// caller's lookup. Mirrors check_rename in dest.py.
export function checkRename(accessor, src, s, dst, d) {
const { store } = accessor;
if (s === d)
return;
if (store.files.has(s)) {
if (store.dirs.has(d))
throw eisdir(dst);
return;
}
if (!store.dirs.has(s))
return;
if (d.startsWith(`${rstripSlash(s)}/`))
throw einval(src);
if (store.files.has(d))
throw enotdir(dst);
const prefix = `${rstripSlash(d)}/`;
if (store.dirs.has(d) && [...store.files.keys(), ...store.dirs].some((k) => k.startsWith(prefix)))
throw enotempty(dst);
}
// The error a lookup of a key the store does not hold answers with.
// open(2) and stat(2) resolve a path one component at a time and stop at the
// first that is not a directory, so a plain file above the key is ENOTDIR
// (`cat a.txt/x` is "Not a directory") and a missing component, or a key that
// is simply absent, is ENOENT. It is the walk checkDestParents makes for a
// destination, so a read, a stat and a write of one path agree on its errno.
// Mirrors lookup_error in dest.py.
export function lookupError(accessor, spec, key) {
return brokenParent(accessor, spec, key) ?? enoent(spec);
}
function brokenParent(accessor, spec, key) {
for (const ancestor of ancestors(key)) {
if (accessor.store.dirs.has(ancestor))
continue;
if (accessor.store.files.has(ancestor))
return enotdir(spec);
return enoent(spec);
}
return null;
}
// Reject a `mkdir` the store cannot satisfy. The companion of
// checkDestParents for the one op that may create its own parents, and the two
// flag modes fail differently because GNU implements them differently:
//
// * Plain mkdir issues one mkdir(2) on the whole path, so an existing target
// is EEXIST whichever kind it is. Its parent chain stays checkDestParents'
// job, which reports the operand because that is what the kernel resolves.
// * mkdir -p walks the chain itself, creating as it goes, so it reports the
// *component* it tripped on rather than the operand: `mkdir -p a.txt/sub`
// is "cannot create directory 'a.txt': Not a directory". Reaching the
// target itself as a plain file is EEXIST, not ENOTDIR. An existing
// directory anywhere in the chain is success, which is what makes -p
// idempotent.
//
// Without this a store has no kernel to refuse a directory key that collides
// with a file key: -p added it anyway, the directory shadowed the file, and
// reading it started reporting EISDIR while the bytes stayed orphaned in the
// store. Pinned against GNU coreutils in docker.
// open(2) for writing answers a directory with EISDIR whatever the caller
// meant to do next, so bash prints `d: Is a directory` for `> d` and `>> d`
// alike and tee and truncate say the same. A real filesystem gets this from
// the kernel; a keyed store has to ask its own directory table, or the bytes
// land on a key the directory shadows and are unreachable from then on.
export function checkWriteTarget(accessor, spec, key) {
if (accessor.store.dirs.has(key))
throw eisdir(spec);
}
export function checkMkdirTarget(accessor, spec, key, parents) {
const store = accessor.store;
if (!parents) {
if (store.dirs.has(key) || store.files.has(key))
throw eexist(spec);
return;
}
for (const component of [...ancestors(key), key]) {
if (!store.files.has(component))
continue;
const named = mountedPath(spec, component);
throw component === key ? eexist(named) : enotdir(named);
}
}
/**
* Throw EDQUOT for a key growing to `size` bytes past its store's capacity,
* or past what its workspace's `max_memory` has left; an uncapped store in an
* uncapped workspace takes any size. Called just before the change, with no
* await between, so the check and the change run as one step. Mirrors
* Python's `check_capacity`.
*/
export function checkCapacity(accessor, spec, key, size) {
const growth = size - (accessor.store.files.get(key)?.byteLength ?? 0);
checkRoom(spec, growth);
const usage = accessor.store.usage;
if (usage !== null)
usage.admit(spec, growth);
}
/**
* Throw EDQUOT for `growth` more bytes past what the running workspace's
* `max_memory` has left; zero or less always passes. Mirrors Python's
* `check_room`.
*/
export function checkRoom(spec, growth) {
const room = memoryRoom();
if (growth > 0 && room !== null && growth > room)
throw edquot(spec);
}
//# sourceMappingURL=dest.js.map