feat: release toolbox portal 0.1.0
This commit is contained in:
93
scripts/path-safety.mjs
Normal file
93
scripts/path-safety.mjs
Normal file
@@ -0,0 +1,93 @@
|
||||
import { lstat, realpath, stat } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
function isMissing(error) {
|
||||
return error && typeof error === 'object' && error.code === 'ENOENT';
|
||||
}
|
||||
|
||||
export async function canonicalExistingPath(candidate, label = 'Path') {
|
||||
const absolute = path.resolve(candidate);
|
||||
try {
|
||||
return await realpath(absolute);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`${label} does not resolve to an existing path: ${absolute}`,
|
||||
{
|
||||
cause: error,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function canonicalProspectivePath(candidate) {
|
||||
const absolute = path.resolve(candidate);
|
||||
const suffix = [];
|
||||
let current = absolute;
|
||||
|
||||
while (true) {
|
||||
let details;
|
||||
try {
|
||||
details = await lstat(current);
|
||||
} catch (error) {
|
||||
if (!isMissing(error)) throw error;
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) throw error;
|
||||
suffix.unshift(path.basename(current));
|
||||
current = parent;
|
||||
continue;
|
||||
}
|
||||
|
||||
let canonical;
|
||||
try {
|
||||
canonical = await realpath(current);
|
||||
} catch (error) {
|
||||
if (details.isSymbolicLink() && isMissing(error)) {
|
||||
throw new Error(`Path contains a dangling symbolic link: ${current}`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (suffix.length > 0) {
|
||||
const canonicalDetails = await stat(canonical);
|
||||
if (!canonicalDetails.isDirectory()) {
|
||||
throw new Error(
|
||||
`Path has a non-directory existing ancestor: ${current}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return path.join(canonical, ...suffix);
|
||||
}
|
||||
}
|
||||
|
||||
export function canonicalPathsOverlap(left, right) {
|
||||
const resolvedLeft = path.resolve(left);
|
||||
const resolvedRight = path.resolve(right);
|
||||
return (
|
||||
resolvedLeft === resolvedRight ||
|
||||
resolvedLeft.startsWith(`${resolvedRight}${path.sep}`) ||
|
||||
resolvedRight.startsWith(`${resolvedLeft}${path.sep}`)
|
||||
);
|
||||
}
|
||||
|
||||
export async function assertSafeRemovalTarget(target) {
|
||||
const canonical = await canonicalProspectivePath(target);
|
||||
const root = path.parse(canonical).root;
|
||||
const protectedPaths = await Promise.all([
|
||||
canonicalProspectivePath(process.cwd()),
|
||||
canonicalProspectivePath(os.homedir()),
|
||||
]);
|
||||
if (
|
||||
canonical === root ||
|
||||
protectedPaths.some(
|
||||
(protectedPath) =>
|
||||
protectedPath === canonical ||
|
||||
protectedPath.startsWith(`${canonical}${path.sep}`)
|
||||
)
|
||||
) {
|
||||
throw new Error(`Refusing unsafe output target: ${canonical}`);
|
||||
}
|
||||
return canonical;
|
||||
}
|
||||
Reference in New Issue
Block a user