@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
105 lines • 4.55 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 { IOResult } from "../../../../io/types.js";
import { encodeText } from "../../../../shell/bytes.js";
import { parseChmod } from "../../../../utils/mode.js";
import { ExecutionNode } from "../../../types.js";
import { scanOptions } from "../getopt.js";
import { builtinUsage, fail } from "../shared.js";
/** Render a mask the way `umask -S` does: the bits it leaves on. */
export function symbolicUmask(mask) {
const perms = 0o777 & ~mask;
const parts = [];
for (const [who, shift] of [
['u', 6],
['g', 3],
['o', 0],
]) {
const bits = (perms >> shift) & 0o7;
const letters = (bits & 4 ? 'r' : '') + (bits & 2 ? 'w' : '') + (bits & 1 ? 'x' : '');
parts.push(`${who}=${letters}`);
}
return parts.join(',');
}
/**
* The mask a `umask` operand names, or the error bash prints for it.
* An all-digit operand is octal, refused with `octal number out of
* range` when it holds an 8 or 9 and clamped to 0777 when octal but too
* large; anything else is a symbolic clause list applied to the
* permissions the current mask leaves on. The two symbolic refusals are
* told apart: a letter outside `ugoa` where an operator was expected is
* `invalid symbolic mode operator`, one outside `rwx` after it is
* `invalid symbolic mode character`.
*/
export function parseUmask(text, current) {
if (/^\d+$/.test(text)) {
if (!/^[0-7]+$/.test(text))
return `bash: umask: ${text}: octal number out of range\n`;
return Math.min(parseInt(text, 8), 0o777);
}
const perms = 0o777 & ~current;
for (const clause of text.split(',')) {
let i = 0;
while (i < clause.length && 'ugoa'.includes(clause[i] ?? ''))
i++;
if (i >= clause.length || !'+-='.includes(clause[i] ?? '')) {
const bad = i < clause.length ? (clause[i] ?? '') : '';
return `bash: umask: \`${bad}': invalid symbolic mode operator\n`;
}
for (const ch of clause.slice(i + 1)) {
if (!'+-=rwx'.includes(ch)) {
return `bash: umask: \`${ch}': invalid symbolic mode character\n`;
}
}
}
const parsed = parseChmod(text, perms);
if (parsed === null)
return `bash: umask: \`${text}': invalid symbolic mode character\n`;
return 0o777 & ~parsed;
}
/**
* Print or set the session's file-creation mask. Octal by default,
* symbolic under `-S`, re-readable under `-p`; with one operand it is
* set for the rest of the shell (a subshell gets its own copy). Extra
* operands are ignored, an unknown option is exit 2, a bad mode exit 1
* with the mask unchanged.
*/
export function handleUmask(args, session) {
const scan = scanOptions(args, 'Sp');
if (scan.bad !== null)
return fail('umask', `bash: umask: ${scan.bad}: invalid option\n${builtinUsage('umask')}`, 2);
const symbolic = scan.letters.includes('S');
const reusable = scan.letters.includes('p');
const operands = scan.operands;
if (operands.length === 0) {
let body = symbolic ? symbolicUmask(session.umask) : session.umask.toString(8).padStart(4, '0');
if (reusable)
body = `umask ${symbolic ? '-S ' : ''}${body}`;
return [
encodeText(body + '\n'),
new IOResult(),
new ExecutionNode({ command: 'umask', exitCode: 0 }),
];
}
const parsed = parseUmask(operands[0] ?? '', session.umask);
if (typeof parsed === 'string')
return fail('umask', parsed, 1);
session.umask = parsed;
return [null, new IOResult(), new ExecutionNode({ command: 'umask', exitCode: 0 })];
}
/** The `umask` arm. */
export function umaskBuiltin(call) {
return Promise.resolve(handleUmask([...call.argv.args], call.context.session));
}
//# sourceMappingURL=umask.js.map