mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-29 13:05:18 +02:00
116 lines
4.7 KiB
JavaScript
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 };
|