UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

230 lines • 10.1 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 { specOf } from "../../spec/builtins.js"; import { FlagView } from "../../spec/flag_view.js"; import { missingOperandError } from "../../spec/usage.js"; import { IOResult } from "../../../io/types.js"; import { FileType, PathSpec } from "../../../types.js"; import { eloop, enoent, enotdir, isMissingPath } from "../../../errors/fs.js"; import { fsErrorLine } from "../../../errors/render.js"; import { parent } from "../../../utils/path.js"; import { dispatchStat, linkTarget } from "../utils/paths.js"; import { encodeText } from "../../../shell/bytes.js"; import { linksOf } from "../utils/links.js"; const MODES = { canonicalize_existing: 'e', canonicalize_missing: 'm' }; const LINKS = { logical: 'L', physical: 'P', strip: 's', no_symlinks: 's' }; // GNU 9.7 resolves a chain of any length (3,000 links measured) and calls a // loop of any length a loop; a walk here stops at LINK_LIMIT links, so a // longer chain is refused as a loop. On a loop, `-m` keeps unresolved the // first link that comes round again counting from the 21st: of // l1 -> l2 -> ... -> lk -> l1, measured for k from 1 to 50, `realpath -m l1` // prints l((20 mod k) + 1), and behind a chain of more than 20 links it // prints the loop's first link. const LINK_LIMIT = 1024; const KEPT_FROM = 21; /** * The link `-m` leaves unresolved once the walk is back at a link: from * `first` on the walk repeats itself every `steps.length - first` links, so * the first link met again from the KEPT_FROM-th on is the loop's own first * link, or the link that step lands on when fewer links lead into the loop. * Mirrors Python's _kept_link. */ function keptLink(steps, first) { const period = steps.length - first; const landing = Math.max(KEPT_FROM - 1, first); const step = steps[first + ((landing - first) % period)]; if (step === undefined) throw new RangeError(`no step ${String(landing)}`); return step; } export function parseFlags(flags) { const fl = new FlagView(flags, specOf('realpath')); const mode = fl.typedOrder(...Object.keys(MODES)).pop(); const links = fl.typedOrder(...Object.keys(LINKS)).pop(); return { mode: mode !== undefined ? (MODES[mode] ?? '') : '', links: links !== undefined ? (LINKS[links] ?? 'P') : 'P', quiet: fl.asBool('quiet'), zero: fl.asBool('zero'), relativeTo: fl.asStr('relative_to') ?? null, relativeBase: fl.asStr('relative_base') ?? null, }; } async function directory(stat, path, word) { if ((await stat(PathSpec.fromStrPath(path))).type !== FileType.DIRECTORY) throw enotdir(word); } /** * A path with its links resolved, the walk `realpath` and `readlink -f` * share. A relative word starts at the working directory. * Each named component is appended and, unless `nolinks`, a link there is * read (`readlink`, one link's target or null) and its target put in front * of the names left, so a `..` climbs from where a link leads. A link the * session cannot see is no link, as a hidden path is no path: `readlink` is * the dispatcher's reader, which answers none for it. A component * followed by `.` or `..` must be a directory. A trailing slash rejects an * existing non-directory. GNU 9.7's default mode accepts a missing final * component with a slash; `nolinks` also accepts missing parents. `e` * requires existence, while `m` checks nothing. A walk that comes back to a * link with the same names left is in a loop, `Too many levels of symbolic * links`, and so is one that meets more than LINK_LIMIT links, which covers * a link that grows the names it leaves (`a -> a/x`) and that GNU walks until * memory runs out. `m` instead leaves one link unresolved and goes on with * the names left after it: on a loop the one keptLink picks, past the limit * the link it stopped at. Throws the first check the walk fails. Mirrors * Python. */ export async function canonicalize(word, cwd, mode, nolinks, readlink, stat) { if (word === '') throw enoent(word); const names = (word.startsWith('/') ? word : `${cwd}/${word}`).split('/').filter((n) => n !== ''); let slash = word.endsWith('/'); let path = '/'; let last; const steps = []; const stepOf = new Map(); for (let name = names.shift(); name !== undefined; name = names.shift()) { last = name; if (name === '.' || name === '..') { if (name === '..') path = parent(path); continue; } path = path === '/' ? `/${name}` : `${path}/${name}`; const target = nolinks || readlink === null ? null : readlink(path); if (target !== null) { const here = [path, [...names]]; const key = JSON.stringify(here); const again = stepOf.get(key); if (again === undefined && steps.length < LINK_LIMIT) { stepOf.set(key, steps.length); steps.push(here); slash = slash || (names.length === 0 && target.endsWith('/')); names.unshift(...target.split('/').filter((n) => n !== '')); path = target.startsWith('/') ? '/' : parent(path); continue; } if (mode !== 'm') throw eloop(word); if (again !== undefined) { const [keptPath, rest] = keptLink(steps, again); path = keptPath; names.splice(0, names.length, ...rest); last = path.slice(path.lastIndexOf('/') + 1); } } const next = names[0]; if (mode !== 'm' && (next === '.' || next === '..')) await directory(stat, path, word); } if (mode === 'm' || last === undefined || last === '.' || last === '..') return path; try { if (slash) await directory(stat, path, word); else await stat(PathSpec.fromStrPath(path)); } catch (err) { if (!isMissingPath(err) || mode === 'e') throw err; if (!nolinks) await directory(stat, parent(path), word); } return path; } function under(base, path) { return base === '/' || path === base || path.startsWith(`${base}/`); } function relative(path, base) { const a = path.split('/').filter((s) => s !== ''); const b = base.split('/').filter((s) => s !== ''); let common = 0; while (common < a.length && common < b.length && a[common] === b[common]) common++; return [...b.slice(common).map(() => '..'), ...a.slice(common)].join('/') || '.'; } /** * Print each operand's canonical path, GNU `realpath` (9.7). An operand * that does not resolve is reported and the rest still print, exit 1. A * `--relative-to` or `--relative-base` directory resolves the same way * first, and one that does not ends the command. Mirrors Python's realpath. */ export async function realpath(paths, stat, cwd = '/', readlink = null, flags = parseFlags({})) { if (paths.length === 0) throw missingOperandError('realpath', null); const canon = async (word) => { const path = await canonicalize(word, cwd, flags.mode, flags.links !== 'P', readlink, stat); if (flags.links !== 'L') return path; return canonicalize(path, cwd, flags.mode, false, readlink, stat); }; const relativeTo = flags.relativeTo ?? flags.relativeBase; let to = null; let base = null; for (const word of new Set([relativeTo, flags.relativeBase])) { if (word === null) continue; let path; try { path = await canon(word); if (flags.mode === 'e') await directory(stat, path, word); } catch (err) { if (!(err instanceof Error)) throw err; return [ null, new IOResult({ exitCode: 1, stderr: encodeText(fsErrorLine('realpath', word, err)) }), ]; } if (word === relativeTo) [to, base] = [path, word === flags.relativeBase ? path : null]; else if (under(path, to ?? '/')) base = path; else [to, base] = [null, to]; } const lines = []; const errors = []; let failed = false; for (const p of paths) { let path; try { path = await canon(p.rawPath); } catch (err) { if (!(err instanceof Error)) throw err; failed = true; if (!flags.quiet) errors.push(fsErrorLine('realpath', p, err)); continue; } const shown = to === null || (base !== null && !under(base, path)); lines.push(shown ? path : relative(path, to ?? '/')); } const end = flags.zero ? '\0' : '\n'; const out = lines.length > 0 ? encodeText(lines.map((l) => l + end).join('')) : null; const stderr = errors.length > 0 ? encodeText(errors.join('')) : null; return [out, new IOResult({ stderr, exitCode: failed ? 1 : 0 })]; } export async function realpathGeneric(paths, texts, ctx, stat) { const pathStat = ctx.vfs !== undefined ? dispatchStat(ctx.vfs.dispatch) : async (path) => (await stat(path)); return realpath(paths, pathStat, ctx.cwd.virtual, linkTarget(linksOf(ctx)), parseFlags(ctx.flags)); } //# sourceMappingURL=realpath.js.map