import { accessSync, constants, realpathSync, statSync } from "node:fs"; import { relative, resolve, sep } from "node:path"; export type LibraryPathAlias = { from: string; to: string; }; export type LibraryPathErrorCode = "LIBRARY_PATH_NOT_FOUND" | "LIBRARY_PATH_NOT_DIRECTORY" | "LIBRARY_PATH_NOT_READABLE"; export class LibraryPathValidationError extends Error { constructor( public readonly code: LibraryPathErrorCode, public readonly path: string ) { super(messageForCode(code)); } } export function resolveLibraryPath(input: string, aliases: LibraryPathAlias[] = []): string { const candidates = candidatePaths(input, aliases); let firstError: LibraryPathValidationError | null = null; for (const candidate of candidates) { try { const stats = statSync(candidate); if (!stats.isDirectory()) { throw new LibraryPathValidationError("LIBRARY_PATH_NOT_DIRECTORY", candidate); } accessSync(candidate, constants.R_OK | constants.X_OK); return realpathSync(candidate); } catch (error) { firstError ??= normalizePathError(error, candidate); } } throw firstError ?? new LibraryPathValidationError("LIBRARY_PATH_NOT_FOUND", resolve(input)); } function candidatePaths(input: string, aliases: LibraryPathAlias[]): string[] { const resolved = resolve(input); const candidates = [resolved]; for (const alias of aliases) { const from = resolve(alias.from); const to = resolve(alias.to); const remainder = relative(from, resolved); if (remainder === "" || (!remainder.startsWith("..") && remainder !== ".." && !remainder.startsWith(`..${sep}`))) { candidates.push(resolve(to, remainder)); } } return [...new Set(candidates)]; } function normalizePathError(error: unknown, path: string): LibraryPathValidationError { if (error instanceof LibraryPathValidationError) return error; const code = typeof error === "object" && error && "code" in error ? String(error.code) : ""; if (code === "ENOENT" || code === "ENOTDIR") { return new LibraryPathValidationError("LIBRARY_PATH_NOT_FOUND", path); } if (code === "EACCES" || code === "EPERM") { return new LibraryPathValidationError("LIBRARY_PATH_NOT_READABLE", path); } return new LibraryPathValidationError("LIBRARY_PATH_NOT_READABLE", path); } function messageForCode(code: LibraryPathErrorCode): string { if (code === "LIBRARY_PATH_NOT_FOUND") return "Library path does not exist"; if (code === "LIBRARY_PATH_NOT_DIRECTORY") return "Library path must be a directory"; return "Library path is not readable"; }