Files
2026-07-13 13:08:41 +08:00

94 lines
2.6 KiB
TypeScript

import fs from "fs";
import path from "path";
import { getAppDataDir, getTempDir } from "./constants";
export class LocalFileAccessError extends Error {
constructor(
message: string,
public readonly code: "INVALID_PATH" | "NOT_FOUND" | "ACCESS_DENIED",
) {
super(message);
this.name = "LocalFileAccessError";
}
}
function resolveBaseDir(baseDir: string): string {
const resolvedBaseDir = path.resolve(baseDir);
try {
return fs.realpathSync(resolvedBaseDir);
} catch {
return resolvedBaseDir;
}
}
function allowedReadableFileBaseDirs(): string[] {
return [getAppDataDir(), getTempDir()].map(resolveBaseDir);
}
function assertPathAllowed(candidatePath: string, baseDirs: string[]): void {
for (const baseDir of baseDirs) {
if (candidatePath === baseDir || candidatePath.startsWith(`${baseDir}${path.sep}`)) {
return;
}
}
throw new LocalFileAccessError(
"Access denied: File path not allowed",
"ACCESS_DENIED",
);
}
export function resolveReadableLocalFile(filePath: unknown): string {
if (typeof filePath !== "string" || filePath.trim().length === 0) {
throw new LocalFileAccessError("Invalid file path", "INVALID_PATH");
}
const requestedPath = path.resolve(filePath);
const allowedBaseDirs = allowedReadableFileBaseDirs();
assertPathAllowed(requestedPath, allowedBaseDirs);
let resolvedPath: string;
try {
resolvedPath = fs.realpathSync(requestedPath);
} catch {
throw new LocalFileAccessError("File not found", "NOT_FOUND");
}
assertPathAllowed(resolvedPath, allowedBaseDirs);
return resolvedPath;
}
export function readReadableLocalFile(filePath: unknown): string {
if (typeof filePath !== "string" || filePath.trim().length === 0) {
throw new LocalFileAccessError("Invalid file path", "INVALID_PATH");
}
const requestedPath = path.resolve(filePath);
const [appDataDir, tempDir] = allowedReadableFileBaseDirs();
if (!(requestedPath.startsWith(appDataDir) || requestedPath.startsWith(tempDir))) {
throw new LocalFileAccessError(
"Access denied: File path not allowed",
"ACCESS_DENIED",
);
}
let resolvedPath: string;
try {
resolvedPath = fs.realpathSync(requestedPath);
} catch {
throw new LocalFileAccessError("File not found", "NOT_FOUND");
}
if (!(resolvedPath.startsWith(appDataDir) || resolvedPath.startsWith(tempDir))) {
throw new LocalFileAccessError(
"Access denied: File path not allowed",
"ACCESS_DENIED",
);
}
assertPathAllowed(resolvedPath, [appDataDir, tempDir]);
return fs.readFileSync(resolvedPath, "utf-8");
}