import { posix as path } from "node:path"; export class InvalidPathError extends Error { constructor(message: string) { super(message); this.name = "InvalidPathError"; } } const ALLOWED_PREFIXES = ["/home/user", "/tmp"]; export function validateAbsolutePath(input: string): string { if (typeof input !== "string" || input.length === 0) { throw new InvalidPathError("Path must be a non-empty string"); } if (input.includes("\0")) { throw new InvalidPathError("Path contains null byte"); } if (!input.startsWith("/")) { throw new InvalidPathError("Path must be absolute"); } const normalized = path.normalize(input); if (normalized.includes("\0")) { throw new InvalidPathError("Path contains null byte after normalization"); } if (normalized === "/" || normalized === "" || normalized.startsWith("..")) { throw new InvalidPathError("Path traversal not allowed"); } // Forbid traversal after normalization (defense in depth) const parts = normalized.split("/"); if (parts.some((p) => p === "..")) { throw new InvalidPathError("Path traversal not allowed"); } if (!isUnderAllowedRoot(normalized)) { throw new InvalidPathError("Path outside allowed namespace"); } return normalized; } function isUnderAllowedRoot(p: string): boolean { return ALLOWED_PREFIXES.some( (root) => p === root || p.startsWith(`${root}/`), ); } export function encodePathForUri(p: string): string { return encodeURIComponent(p); } export function decodePathFromUri(p: string): string { return decodeURIComponent(p); }