UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

245 lines • 10.3 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 { readdirOp } from "../../../../commands/builtin/generic/crossmount/utils.js"; import { dispatchedReadTree } from "../../../../commands/builtin/generic_bind/files.js"; import { treeWalk, WalkBudget } from "../../../../commands/builtin/utils/tree.js"; import { specOf } from "../../../../commands/spec/builtins.js"; import { FlagView } from "../../../../commands/spec/flag_view.js"; import { parseCommand, parseToKwargs } from "../../../../commands/spec/parser.js"; import { callerSignal } from "../../../../context/session_context.js"; import { classify } from "../../../../errors/index.js"; import { encodeText } from "../../../../shell/bytes.js"; import { FileStat, FileType, PathSpec, wordText } from "../../../../types.js"; import { encodeBase64 } from "../../../../utils/base64.js"; import { toHex } from "../../../../utils/hex.js"; import { rstripSlash } from "../../../../utils/slash.js"; import { result } from "../shared.js"; import { attrError, attrOperands, attrUsageRefusal, GETFATTR_USAGE } from "./xattr.js"; const ENCODINGS = new Set(['text', 'hex', 'base64']); const DEFAULT_MATCH = '^user\\.'; /** * One value the way getfattr prints it after `name=`. With no `-e` the * value is text when at most one byte in eight is unprintable (a * trailing NUL not counted) and base64 otherwise. Text escapes NUL, * newline and carriage return as octal and `"` and `\` with a backslash, * and passes every other byte through raw; a trailing NUL is dropped. * Mirrors Python's `encode_value`. */ export function encodeValue(value, encoding) { const body = value.at(-1) === 0 ? value.subarray(0, -1) : value; let chosen = encoding; if (chosen === null) { const unprintable = body.filter((b) => b < 0x20 || b > 0x7e).length; chosen = body.length >= unprintable * 8 ? 'text' : 'base64'; } if (chosen === 'hex') { return encodeText('0x' + toHex(value)); } if (chosen === 'base64') return encodeText('0s' + encodeBase64(value)); const out = [0x22]; for (const b of body) { if (b === 0 || b === 0x0a || b === 0x0d) for (const item of encodeText('\\' + b.toString(8).padStart(3, '0'))) out.push(item); else if (b === 0x22 || b === 0x5c) out.push(0x5c, b); else out.push(b); } out.push(0x22); return Uint8Array.from(out); } /** * A subtree in pre-order, the way nftw hands it to getfattr -R. Every * entry is reported, links included. A link to a directory is descended * only when `deref` says so: for every entry under `-L`, and for an * operand itself unless `-P`. `calls` are the walk's readdir and lstat, * answered from a mount's `readTree` where one answers; `dispatch` follows a * link. Mirrors Python's `_walk`. */ async function walk(dispatch, calls, links, path, shown, logical, deref, budget) { const [readdir, lstat] = calls; const entries = [[path, shown]]; let stat; try { stat = await lstat(path); if (stat instanceof FileStat && stat.type === FileType.SYMLINK && deref) { ; [stat] = await dispatch('stat', path); } } catch (err) { const code = classify(err); if (code === 'ENOENT' || code === 'ENOTDIR') return entries; throw err; } if (!(stat instanceof FileStat) || stat.type !== FileType.DIRECTORY) return entries; const here = links.follow(path.virtual); if (!budget.opens(here)) return entries; const limit = budget.listingLimit(here); const children = await (limit === null ? readdir(path) : readdir(path, false, limit)); for (const child of children) { if (!budget.spend(here)) break; const bare = rstripSlash(child); const name = bare.slice(bare.lastIndexOf('/') + 1); const below = `${rstripSlash(shown)}/${name}`; entries.push(...(await walk(dispatch, calls, links, PathSpec.fromStrPath(bare), below, logical, logical, budget))); } return entries; } /** * One path's output (its `# file:` block, or its bare values), and * whether an attribute it was asked for is not set. An attribute that is * not set is reported per name and skipped; any other failure (the path * is missing) is the caller's to report. Mirrors Python's `_file_block`. */ async function fileBlock(dispatch, path, label, header, want, errors) { let names; if (want.name !== null) { names = [want.name]; } else { const [listed] = (await dispatch('listxattr', path, [], { nofollow: want.nofollow })); names = listed.filter((n) => want.matcher === null || want.matcher.test(n)); } const out = []; const block = []; let missing = false; for (const attr of names) { if (!want.dump && !want.onlyValues) { for (const item of encodeText(`${attr}\n`)) block.push(item); continue; } let value; try { ; [value] = (await dispatch('getxattr', path, [], { name: attr, nofollow: want.nofollow, })); } catch (err) { if (classify(err) !== 'NO_XATTR') throw err; errors.push(`${label}: ${attr}: ${attrError(err)}\n`); missing = true; continue; } if (want.onlyValues) for (const item of value) out.push(item); else block.push(...encodeText(`${attr}=`), ...encodeValue(value, want.encoding), 0x0a); } if (block.length > 0) out.push(...encodeText(`# file: ${header}\n`), ...block, 0x0a); return [Uint8Array.from(out), missing]; } /** * getfattr: print the extended attributes of each path. * * Debian's attr 2.5.2, pinned in docker: a `# file:` block per path that * has a matching attribute, names sorted, a blank line after each block, * `-d`/`-n` adding `="value"`, and the default match `^user\.` (`-m -` * matches every name). The attributes are the dispatcher's: what was set on * the path. `-h` reads a link's own attributes. `-R` stops after * `walkLimit` entries of a mount, as `ls -R` does, and exits 1. * Mirrors Python's `handle_getfattr`. */ export async function handleGetfattr(namespace, dispatch, session, args) { const spec = specOf('getfattr'); const parsed = parseCommand(spec, args.map((a) => wordText(a)), session.cwd, 'getfattr'); const refused = attrUsageRefusal('getfattr', parsed, GETFATTR_USAGE); if (refused !== null) return refused; const fl = new FlagView(parseToKwargs(parsed), spec); const encoding = fl.asStr('encoding') ?? null; const targets = attrOperands(parsed); if ((encoding !== null && !ENCODINGS.has(encoding)) || targets.length === 0) { return result('getfattr', { exitCode: 2, stderr: GETFATTR_USAGE }); } const pattern = fl.asStr('match') ?? DEFAULT_MATCH; let matcher = null; if (pattern !== '-') { try { matcher = new RegExp(pattern); } catch { return result('getfattr', { exitCode: 1, stderr: `getfattr: invalid regular expression "${pattern}"\n`, }); } } const name = fl.asStr('name') ?? null; const want = { name, matcher, dump: fl.asBool('dump') || name !== null, onlyValues: fl.asBool('only_values'), encoding, nofollow: fl.asBool('no_dereference'), }; const absolute = fl.asBool('absolute_names'); const out = []; const errors = []; let failed = false; let warned = false; const links = namespace.sessionLinks(session); const mounts = links.mounts(true); const calls = treeWalk(dispatchedReadTree(dispatch, links.mounts(false), (virtual) => links.followParent(virtual), true), readdirOp(dispatch), async (path) => (await dispatch('stat', path, [], { nofollow: true }))[0], true, callerSignal()); for (const target of targets) { const typed = target.rawPath; const budget = new WalkBudget(target, mounts); const entries = fl.asBool('recursive') ? await walk(dispatch, calls, links, target, typed, fl.asBool('logical'), !fl.asBool('physical'), budget) : [[target, typed]]; for (const [path, label] of entries) { const header = !absolute && label.startsWith('/') ? label.replace(/^\/+/, '') || '.' : label; let block; let missing; try { ; [block, missing] = await fileBlock(dispatch, path, label, header, want, errors); } catch (err) { errors.push(`getfattr: ${label}: ${attrError(err)}\n`); failed = true; continue; } if (block.length > 0 && header !== label && !want.onlyValues && !warned) { errors.push("getfattr: Removing leading '/' from absolute path names\n"); warned = true; } for (const item of block) out.push(item); failed = failed || missing; } for (const line of budget.stopped) errors.push(`getfattr: ${line}\n`); failed = failed || budget.stopped.length > 0; } return result('getfattr', { out: out.length > 0 ? Uint8Array.from(out) : null, exitCode: failed ? 1 : 0, stderr: errors.join(''), }); } //# sourceMappingURL=getfattr.js.map