Files
ECC/scripts/lib/install/guarded-write.js
T

116 lines
4.7 KiB
JavaScript

'use strict';
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { sameFileIdentity } = require('./claude-settings-lock');
function snapshotExpectedContent(expectedContent) {
if (expectedContent === undefined) return undefined;
if (expectedContent && expectedContent.kind === 'absent') {
return Object.freeze({ kind: 'absent' });
}
if (expectedContent && expectedContent.kind === 'sha256'
&& typeof expectedContent.digest === 'string' && /^[a-f0-9]{64}$/i.test(expectedContent.digest)) {
return Object.freeze({ kind: 'sha256', digest: expectedContent.digest.toLowerCase() });
}
throw new TypeError('Invalid expectedContent for guarded write.');
}
function changedDestination(action, filePath, expectedContent, cause) {
const message = expectedContent
? `Refusing OpenCode hook deactivation: activation changed after preflight at ${filePath}`
: `Refusing to ${action}: managed destination changed during the write.`;
const error = new Error(message);
if (cause) {
error.cause = cause;
if (cause.code) error.code = cause.code;
}
return error;
}
function writeFileNoFollow(filePath, content, {
mode,
action = 'write managed file',
validateDestination,
expectedContent: requestedContent,
} = {}) {
if (typeof validateDestination !== 'function') {
throw new TypeError('writeFileNoFollow requires validateDestination.');
}
const destination = path.resolve(filePath);
const expectedContent = snapshotExpectedContent(requestedContent);
const bytes = typeof content === 'string' ? Buffer.from(content) : content;
if (!Buffer.isBuffer(bytes)) throw new TypeError('Managed file content must be a string or Buffer.');
const changed = cause => changedDestination(action, destination, expectedContent, cause);
function validatePath() {
const validated = validateDestination(destination);
if (typeof validated !== 'string' || path.resolve(validated) !== destination) throw changed();
}
function parentStats() {
const stats = fs.lstatSync(path.dirname(destination), { bigint: true });
if (!stats.isDirectory() || stats.isSymbolicLink()) throw changed();
return stats;
}
validatePath();
const parent = parentStats();
const flags = (expectedContent ? fs.constants.O_RDWR : fs.constants.O_WRONLY)
| (!expectedContent || expectedContent.kind === 'absent' ? fs.constants.O_CREAT : 0)
| (expectedContent && expectedContent.kind === 'absent' ? fs.constants.O_EXCL : 0)
| (fs.constants.O_NOFOLLOW || 0);
let descriptor;
try {
descriptor = fs.openSync(destination, flags, mode);
} catch (error) {
if (expectedContent) throw changed(error);
throw error;
}
function assertPinned() {
validatePath();
const descriptorStat = fs.fstatSync(descriptor, { bigint: true });
const liveStat = fs.lstatSync(destination, { bigint: true });
if (!sameFileIdentity(parent, parentStats()) || !descriptorStat.isFile()
|| !liveStat.isFile() || liveStat.isSymbolicLink() || !sameFileIdentity(descriptorStat, liveStat)) {
throw changed();
}
return descriptorStat;
}
let primaryError;
try {
const before = assertPinned();
if (expectedContent && expectedContent.kind === 'absent' && before.size !== 0n) throw changed();
if (expectedContent && expectedContent.kind === 'sha256') {
const digest = crypto.createHash('sha256').update(fs.readFileSync(descriptor)).digest('hex');
const after = assertPinned();
if (!sameFileIdentity(before, after) || before.size !== after.size
|| before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs
|| digest !== expectedContent.digest) throw changed();
}
// Observed changes are rejected before truncation. This is not a CAS against
// arbitrary editors: callers coordinate participating writers with a lease.
fs.ftruncateSync(descriptor, 0);
for (let offset = 0; offset < bytes.length;) {
const written = fs.writeSync(descriptor, bytes, offset, bytes.length - offset, offset);
if (!Number.isInteger(written) || written <= 0 || written > bytes.length - offset) {
throw new Error(`Refusing to ${action}: file write made no valid progress.`);
}
offset += written;
}
if (mode !== undefined) fs.fchmodSync(descriptor, mode);
} catch (error) {
primaryError = error;
} finally {
try {
fs.closeSync(descriptor);
} catch (error) {
if (primaryError) primaryError.closeError = error;
else primaryError = error;
}
}
// An exclusive creation that later fails is not unlinked: the pathname may
// already belong to another writer. Keep the refusal visible to the caller.
if (primaryError) throw primaryError;
}
module.exports = { writeFileNoFollow };