import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
export const DEFAULT_STATE_DIR =
process.env.CONSCIOUSNESS_EXPLORER_STATE_DIR ||
path.join(os.homedir(), '.consciousness-explorer', 'state');
const WINDOWS_RESERVED = new Set([
'CON', 'PRN', 'AUX', 'NUL',
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
]);
export class SafePathError extends Error {
constructor(message, code = 'EUNSAFE_PATH') {
super(message);
this.name = 'SafePathError';
this.code = code;
}
}
export function assertSafeBasename(name) {
if (typeof name !== 'string' || name.length === 0) {
throw new SafePathError('filepath must be a non-empty string', 'EINVAL');
}
if (Buffer.byteLength(name, 'utf8') > 255) {
throw new SafePathError('filepath basename exceeds 255 bytes', 'ENAMETOOLONG');
}
if (name.includes('/') || name.includes('\\')) {
throw new SafePathError(
`filepath must be a basename without path separators (got "${name}")`,
'EPATHSEP',
);
}
if (name === '.' || name === '..') {
throw new SafePathError(`filepath "${name}" is not allowed`, 'EDOTSEG');
}
if (name.startsWith('.')) {
throw new SafePathError(
`filepath must not start with "." (got "${name}")`,
'EHIDDEN',
);
}
for (let i = 0; i < name.length; i++) {
const c = name.charCodeAt(i);
if (c < 0x20 || c === 0x7f) {
throw new SafePathError(
`filepath contains control character (0x${c.toString(16)})`,
'ECONTROL',
);
}
}
const stem = name.split('.')[0].toUpperCase();
if (WINDOWS_RESERVED.has(stem)) {
throw new SafePathError(
`filepath uses Windows-reserved device name "${stem}"`,
'ERESERVED',
);
}
}
export function resolveStatePath(filename, { stateDir = DEFAULT_STATE_DIR } = {}) {
assertSafeBasename(filename);
const baseAbs = path.resolve(stateDir);
fs.mkdirSync(baseAbs, { recursive: true, mode: 0o700 });
const candidate = path.resolve(baseAbs, filename);
const rel = path.relative(baseAbs, candidate);
if (rel.startsWith('..') || path.isAbsolute(rel) || rel.includes(`..${path.sep}`)) {
throw new SafePathError(
`resolved path "${candidate}" escapes state dir "${baseAbs}"`,
'EOUTSIDE',
);
}
return candidate;
}
export function openSafeWriteFd(absPath) {
const flags =
fs.constants.O_WRONLY |
fs.constants.O_CREAT |
fs.constants.O_TRUNC |
(fs.constants.O_NOFOLLOW || 0) |
(fs.constants.O_CLOEXEC || 0);
return fs.openSync(absPath, flags, 0o600);
}
export function openSafeReadFd(absPath) {
const flags =
fs.constants.O_RDONLY |
(fs.constants.O_NOFOLLOW || 0) |
(fs.constants.O_CLOEXEC || 0);
return fs.openSync(absPath, flags);
}
export function safeWriteState(filename, data, options) {
const abs = resolveStatePath(filename, options);
const fd = openSafeWriteFd(abs);
try {
fs.writeSync(fd, typeof data === 'string' ? data : Buffer.from(data));
} finally {
fs.closeSync(fd);
}
return abs;
}
export function safeReadState(filename, options) {
const abs = resolveStatePath(filename, options);
const fd = openSafeReadFd(abs);
try {
const stat = fs.fstatSync(fd);
if (!stat.isFile()) {
throw new SafePathError(
`state path "${abs}" is not a regular file`,
'ENOTFILE',
);
}
const buf = Buffer.allocUnsafe(stat.size);
let read = 0;
while (read < stat.size) {
read += fs.readSync(fd, buf, read, stat.size - read, null);
}
return buf.toString('utf8');
} finally {
fs.closeSync(fd);
}
}