UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

144 lines • 6.88 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 { 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