@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
151 lines • 6.02 kB
JavaScript
import { IOResult } from "../../../io/types.js";
import { eisdir, enoent, enotdir, isEnotdir, isFsError } from "../../../errors/fs.js";
import { classify } from "../../../errors/classify.js";
import { fsErrorLine } from "../../../errors/render.js";
import { posixPhrase } from "../../../errors/posix.js";
import { shellQuoteAlways } from "../../../utils/quote.js";
import { isDir } from "../../../utils/stat_view.js";
import { UsageError } from "../../errors.js";
import { usageHint } from "../../spec/usage.js";
import { specOf } from "../../spec/builtins.js";
import { FlagView } from "../../spec/flag_view.js";
import {} from "../../spec/types.js";
import { absentDestError } from "../utils/paths.js";
import { endsAtDot } from "../../../utils/path.js";
import { encodeText } from "../../../shell/bytes.js";
import { StreamRefusal } from "../../../rs/errors.js";
import { refused, scan } from "../../../rs/scan.js";
const MODES = new Set([
'absolute',
'extend',
'reduce',
'at_most',
'at_least',
'round_down',
'round_up',
]);
/** The length `size` asks for, given a file's current one. Mirrors Python's `resolve_size`. */
export function resolveSize(size, current) {
const number = size.number;
if (size.mode === 'extend')
return current + number;
if (size.mode === 'reduce')
return current > number ? current - number : 0n;
if (size.mode === 'at_most')
return current < number ? current : number;
if (size.mode === 'at_least')
return current > number ? current : number;
if (size.mode === 'round_down')
return current - (current % number);
if (size.mode === 'round_up')
return ((current + number - 1n) / number) * number;
return number;
}
/**
* truncate's options, `-s` read by mirage-rs before any operand is resolved:
* the flags, or truncate's refusal. Mirrors Python's `parse_flags`.
*/
export function parseFlags(bag) {
const fl = new FlagView(bag, specOf('truncate'));
let values;
try {
values = scan('truncate', fl.texts('size'));
}
catch (err) {
if (!(err instanceof StreamRefusal))
throw err;
return refused(err);
}
const size = values.size;
if (size === null ||
typeof size !== 'object' ||
Array.isArray(size) ||
typeof size.mode !== 'string' ||
!MODES.has(size.mode) ||
typeof size.size !== 'string') {
throw new Error(`truncate: mirage-rs read -s as ${JSON.stringify(size)}`);
}
return {
size: { mode: size.mode, number: BigInt(size.size) },
noCreate: fl.asBool('no_create'),
};
}
// Set each operand's length, GNU `truncate -s`. Every operand is tried,
// and one GNU cannot open is reported in its words and the rest still go
// (exit 1): `cannot open 'x' for writing` for any open failure, a
// directory's EISDIR included. Mirrors Python's truncate.
export async function truncateGeneric(paths, flags, stat, truncate) {
if (paths.length === 0)
throw new UsageError(`truncate: missing file operand\n${usageHint('truncate')}`, 1);
const errors = [];
for (const path of paths) {
try {
const line = await truncateOne(path, flags, stat, truncate);
if (line !== null)
errors.push(line);
}
catch (e) {
if (!isFsError(e))
throw e;
errors.push(fsErrorLine('truncate', path, e));
}
}
const err = errors.join('');
return [
null,
new IOResult({ exitCode: err === '' ? 0 : 1, stderr: err === '' ? null : encodeText(err) }),
];
}
// One operand, in the order GNU's open settles it. GNU opens the name
// before it looks at anything, with O_CREAT unless -c: an absent file is
// made (-c leaves it, silently), but only in a directory that exists, and
// a plain file in the chain is ENOTDIR either way. The size is read first
// here only because a relative spec needs it, so a stat that misses is not
// the verdict: the chain is, walked the way cp walks a destination's, since
// a backend's write would make a key under any parent at all. A directory,
// and a name typed with a slash in a directory that exists, is the open's
// EISDIR, settled here so a backend with no truncate op answers in GNU's
// words too: `missing/` and `reg/` are both `Is a directory` and nothing is
// made, while under -c `reg/` goes to the truncate op, whose lookup is
// ENOTDIR. A length that cannot take (a pipe has none) is the ftruncate
// step's EINVAL, which GNU words with the size it asked for, and that line
// is returned. Mirrors Python's _truncate_one.
async function truncateOne(path, flags, stat, truncate) {
let current = 0n;
let directory = false;
try {
const st = await stat(path);
current = BigInt(st.size ?? 0);
directory = isDir(st);
}
catch (err) {
const code = err.code;
if (code !== 'ENOENT' && code !== 'ENOTDIR')
throw err;
if (isEnotdir(err) && (flags.noCreate || !path.rawPath.endsWith('/')))
throw err;
const why = await absentDestError(stat, path);
if (why === 'ENOTDIR')
throw enotdir(path);
if (flags.noCreate)
return null;
if (why !== null)
throw enoent(path);
current = 0n;
}
const slashed = path.rawPath.endsWith('/') && !endsAtDot(path.rawPath);
if (directory || (slashed && !flags.noCreate))
throw eisdir(path);
const size = resolveSize(flags.size, current);
try {
await truncate(path, Number(size), flags.noCreate);
}
catch (err) {
if (classify(err) !== 'EINVAL')
throw err;
const label = shellQuoteAlways(path.rawPath === '' ? path.virtual : path.rawPath);
return `truncate: failed to truncate ${label} at ${size.toString()} bytes: ${posixPhrase('EINVAL')}\n`;
}
return null;
}
//# sourceMappingURL=truncate.js.map