@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
80 lines • 3.7 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 { FileType } from "../../../types.js";
import { posixPhrase } from "../../../errors/posix.js";
import { CycleError } from "../../../utils/path.js";
// Whether an operand typed with a trailing slash names a symlink.
export function isSlashedLink(p, links) {
return links !== null && p.rawPath.endsWith('/') && links.statAt(p.virtual) !== null;
}
// GNU's refusal for an `rm` operand that is a slashed symlink.
//
// A link typed with a trailing slash is refused, never followed: the
// dispatcher deliberately left the link entry in place so the command can
// report it rather than delete what the slash was protecting. GNU splits the
// wording by what the slash resolved to and whether -r was given: a directory
// without -r is EISDIR and -f does not suppress it, everything else is ENOTDIR
// and -f does.
//
// Returns null when the operand is not a slashed link, or when -f silences the
// refusal; the caller then proceeds as usual.
export async function rmLinkRefusal(p, links, opts) {
if (!isSlashedLink(p, links) || links === null)
return null;
const target = await links.targetStat(p.virtual);
if (target !== null && target.type === FileType.DIRECTORY && !opts.recursive) {
return `rm: cannot remove '${p.rawPath}': Is a directory`;
}
if (opts.force)
return null;
return `rm: cannot remove '${p.rawPath}': Not a directory`;
}
// GNU's refusal for a `mkdir` operand occupied by a symlink.
//
// mkdir(2) lstats the name it is about to create, so a symlink sitting there is
// EEXIST however it was spelled: no backend can see the link, so the name plane
// has to answer. -p is satisfied only when the link already leads to a
// directory; pointing at a file or at nothing still collides (GNU
// `mkdir -p dangle` is "File exists", not a fresh directory at the link's
// target).
//
// `taken` says the name is already occupied by a link, so the caller must not
// create anything; `message` is the line to report (null when -p is already
// satisfied and the operand is simply skipped).
export async function mkdirLinkRefusal(p, links, opts) {
if (links?.statAt(p.virtual) == null) {
return { taken: false, message: null };
}
if (opts.parents) {
// -p stats the name mkdir(2) found taken, to see whether it is the
// directory asked for, and a loop fails that stat.
try {
links.resolve(p.virtual);
}
catch (err) {
if (!(err instanceof CycleError))
throw err;
return { taken: true, message: `mkdir: cannot stat '${p.rawPath}': ${posixPhrase('ELOOP')}` };
}
}
const target = await links.targetStat(p.virtual);
if (opts.parents && target !== null && target.type === FileType.DIRECTORY) {
return { taken: true, message: null };
}
return {
taken: true,
message: `mkdir: cannot create directory '${p.rawPath}': File exists`,
};
}
//# sourceMappingURL=slash_links.js.map