import fs from "node:fs" ;
import { sameFileIdentity as hasSameFileIdentity } from "./file-identity.js" ;
export type SafeOpenSyncFailureReason = "path" | "validation" | "io" ;
export type SafeOpenSyncResult =
| { ok: true ; path: string; fd: number; stat: fs.Stats }
| { ok: false ; reason: SafeOpenSyncFailureReason; error?: unknown };
export type SafeOpenSyncAllowedType = "file" | "directory" ;
type SafeOpenSyncFs = Pick<
typeof fs,
"constants" | "lstatSync" | "realpathSync" | "openSync" | "fstatSync" | "closeSync"
>;
function isExpectedPathError(error: unknown): boolean {
const code =
typeof error === "object" && error !== null && "code" in error ? String(error.code) : "" ;
return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP" ;
}
export function sameFileIdentity(left: fs.Stats, right: fs.Stats): boolean {
return hasSameFileIdentity(left, right);
}
export function openVerifiedFileSync(params: {
filePath: string;
resolvedPath?: string;
rejectPathSymlink?: boolean ;
rejectHardlinks?: boolean ;
maxBytes?: number;
allowedType?: SafeOpenSyncAllowedType;
ioFs?: SafeOpenSyncFs;
}): SafeOpenSyncResult {
const ioFs = params.ioFs ?? fs;
const allowedType = params.allowedType ?? "file" ;
const openReadFlags =
ioFs.constants.O_RDONLY |
(typeof ioFs.constants.O_NOFOLLOW === "number" ? ioFs.constants.O_NOFOLLOW : 0 );
let fd: number | null = null ;
try {
if (params.rejectPathSymlink) {
const candidateStat = ioFs.lstatSync(params.filePath);
if (candidateStat.isSymbolicLink()) {
return { ok: false , reason: "validation" };
}
}
const realPath = params.resolvedPath ?? ioFs.realpathSync(params.filePath);
const preOpenStat = ioFs.lstatSync(realPath);
if (!isAllowedType(preOpenStat, allowedType)) {
return { ok: false , reason: "validation" };
}
if (params.rejectHardlinks && preOpenStat.isFile() && preOpenStat.nlink > 1 ) {
return { ok: false , reason: "validation" };
}
if (
params.maxBytes !== undefined &&
preOpenStat.isFile() &&
preOpenStat.size > params.maxBytes
) {
return { ok: false , reason: "validation" };
}
fd = ioFs.openSync(realPath, openReadFlags);
const openedStat = ioFs.fstatSync(fd);
if (!isAllowedType(openedStat, allowedType)) {
return { ok: false , reason: "validation" };
}
if (params.rejectHardlinks && openedStat.isFile() && openedStat.nlink > 1 ) {
return { ok: false , reason: "validation" };
}
if (params.maxBytes !== undefined && openedStat.isFile() && openedStat.size > params.maxBytes) {
return { ok: false , reason: "validation" };
}
if (!sameFileIdentity(preOpenStat, openedStat)) {
return { ok: false , reason: "validation" };
}
const opened = { ok: true as const , path: realPath, fd, stat: openedStat };
fd = null ;
return opened;
} catch (error) {
if (isExpectedPathError(error)) {
return { ok: false , reason: "path" , error };
}
return { ok: false , reason: "io" , error };
} finally {
if (fd !== null ) {
ioFs.closeSync(fd);
}
}
}
function isAllowedType(stat: fs.Stats, allowedType: SafeOpenSyncAllowedType): boolean {
if (allowedType === "directory" ) {
return stat.isDirectory();
}
return stat.isFile();
}
Messung V0.5 in Prozent C=96 H=95 G=95
¤ Dauer der Verarbeitung: 0.4 Sekunden
¤
*© Formatika GbR, Deutschland