Add plugins/common.ts for the helpers and size limits copied across plugin files; managed-transaction compares snapshots in one place; guard template imported directly. Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
74 lines
2.4 KiB
TypeScript
74 lines
2.4 KiB
TypeScript
import { constants } from 'node:fs';
|
|
import fsp from 'node:fs/promises';
|
|
import type { FileHandle } from 'node:fs/promises';
|
|
import { asError, errorMessage, hasCode, MAX_SOURCE_BYTES } from './common.js';
|
|
|
|
export interface FileSnapshot {
|
|
readonly contents: string;
|
|
readonly device: bigint | number;
|
|
readonly inode: bigint | number;
|
|
readonly mode: number;
|
|
}
|
|
|
|
export async function readPhysicalFile(
|
|
filePath: string,
|
|
label: string,
|
|
maximumBytes = MAX_SOURCE_BYTES,
|
|
): Promise<FileSnapshot | undefined> {
|
|
let before;
|
|
try {
|
|
before = await fsp.lstat(filePath, { bigint: true });
|
|
} catch (error) {
|
|
if (hasCode(error, 'ENOENT') || hasCode(error, 'ENOTDIR')) return undefined;
|
|
throw new Error(`Inspect ${label} at ${filePath}: ${errorMessage(error)}`, {
|
|
cause: asError(error),
|
|
});
|
|
}
|
|
if (!before.isFile() || before.isSymbolicLink()) {
|
|
throw new Error(`${label} path is not a physical file: ${filePath}`);
|
|
}
|
|
if (before.size > BigInt(maximumBytes)) {
|
|
throw new Error(`${label} exceeds ${maximumBytes} bytes: ${filePath}`);
|
|
}
|
|
let flags = constants.O_RDONLY;
|
|
if (process.platform !== 'win32' && typeof constants.O_NOFOLLOW === 'number') {
|
|
flags |= constants.O_NOFOLLOW;
|
|
}
|
|
let handle: FileHandle | undefined;
|
|
try {
|
|
handle = await fsp.open(filePath, flags);
|
|
const after = await handle.stat({ bigint: true });
|
|
if (!after.isFile() || before.dev !== after.dev || before.ino !== after.ino) {
|
|
throw new Error(`${label} changed while opening: ${filePath}`);
|
|
}
|
|
const contents = await handle.readFile('utf-8');
|
|
if (Buffer.byteLength(contents) > maximumBytes) {
|
|
throw new Error(`${label} exceeds ${maximumBytes} bytes: ${filePath}`);
|
|
}
|
|
return {
|
|
contents,
|
|
device: after.dev,
|
|
inode: after.ino,
|
|
mode: Number(after.mode & 0o777n),
|
|
};
|
|
} finally {
|
|
await handle?.close();
|
|
}
|
|
}
|
|
|
|
export async function inspectPhysicalDirectory(directory: string, label: string): Promise<boolean> {
|
|
let metadata;
|
|
try {
|
|
metadata = await fsp.lstat(directory);
|
|
} catch (error) {
|
|
if (hasCode(error, 'ENOENT') || hasCode(error, 'ENOTDIR')) return false;
|
|
throw new Error(`Inspect ${label} at ${directory}: ${errorMessage(error)}`, {
|
|
cause: asError(error),
|
|
});
|
|
}
|
|
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
throw new Error(`${label} is not a physical directory: ${directory}`);
|
|
}
|
|
return true;
|
|
}
|