src / fsutil.ts
import { promises as fs } from "fs";
import * as path from "path";
export const MAX_FILE_BYTES = 5 * 1024 * 1024;
/**
* Heuristic: a NUL byte, or >30% control characters, means binary. Bytes >= 128
* are not counted so UTF-8 text in any script is not misflagged.
*/
export function looksBinary(sample: Buffer): boolean {
if (sample.includes(0)) return true;
if (sample.length === 0) return false;
let control = 0;
for (const b of sample) {
const isText = b >= 128 || (b >= 32 && b < 127) || b === 8 || b === 9 || b === 10 || b === 12 || b === 13;
if (!isText) control++;
}
return control / sample.length > 0.3;
}
/** Map a Node fs error to a plain message that never leaks the real path. */
export function fsError(e: unknown, userPath: string): string {
const code = (e as NodeJS.ErrnoException).code;
switch (code) {
case "ENOENT":
return `Error: no such file: ${userPath}`;
case "EACCES":
case "EPERM":
return `Error: permission denied: ${userPath}`;
case "EISDIR":
return `Error: is a directory, not a file: ${userPath}`;
case "ENOTDIR":
return `Error: not a directory in path: ${userPath}`;
default:
return `Error: cannot access ${userPath} (${code ?? "unknown"})`;
}
}
/** Write via temp file in the same directory + rename, so a crash never leaves a partial file. */
export async function atomicWrite(abs: string, data: string): Promise<void> {
const tmp = path.join(path.dirname(abs), `.${path.basename(abs)}.${process.pid}.${Date.now()}.tmp`);
try {
await fs.writeFile(tmp, data, "utf8");
const existing = await fs.stat(abs).catch(() => null);
if (existing) await fs.chmod(tmp, existing.mode & 0o777);
await fs.rename(tmp, abs);
} catch (e) {
await fs.rm(tmp, { force: true });
throw e;
}
}
src / fsutil.ts
import { promises as fs } from "fs";
import * as path from "path";
export const MAX_FILE_BYTES = 5 * 1024 * 1024;
/**
* Heuristic: a NUL byte, or >30% control characters, means binary. Bytes >= 128
* are not counted so UTF-8 text in any script is not misflagged.
*/
export function looksBinary(sample: Buffer): boolean {
if (sample.includes(0)) return true;
if (sample.length === 0) return false;
let control = 0;
for (const b of sample) {
const isText = b >= 128 || (b >= 32 && b < 127) || b === 8 || b === 9 || b === 10 || b === 12 || b === 13;
if (!isText) control++;
}
return control / sample.length > 0.3;
}
/** Map a Node fs error to a plain message that never leaks the real path. */
export function fsError(e: unknown, userPath: string): string {
const code = (e as NodeJS.ErrnoException).code;
switch (code) {
case "ENOENT":
return `Error: no such file: ${userPath}`;
case "EACCES":
case "EPERM":
return `Error: permission denied: ${userPath}`;
case "EISDIR":
return `Error: is a directory, not a file: ${userPath}`;
case "ENOTDIR":
return `Error: not a directory in path: ${userPath}`;
default:
return `Error: cannot access ${userPath} (${code ?? "unknown"})`;
}
}
/** Write via temp file in the same directory + rename, so a crash never leaves a partial file. */
export async function atomicWrite(abs: string, data: string): Promise<void> {
const tmp = path.join(path.dirname(abs), `.${path.basename(abs)}.${process.pid}.${Date.now()}.tmp`);
try {
await fs.writeFile(tmp, data, "utf8");
const existing = await fs.stat(abs).catch(() => null);
if (existing) await fs.chmod(tmp, existing.mode & 0o777);
await fs.rename(tmp, abs);
} catch (e) {
await fs.rm(tmp, { force: true });
throw e;
}
}