mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-29 04:55:12 +02:00
Preserve contributor history and current-main behavior while resolving the exact reviewed follow-up.
Source-PR: https://github.com/affaan-m/ECC/pull/3008
Source-Parent: ef8c2d8b9c
Review-Manifest-SHA256: 6189690fc7dd826fda286e46a5d3ec8d50b6049740f74f0dbd4490a36a76854c
297 lines
14 KiB
JavaScript
297 lines
14 KiB
JavaScript
/** Focused descriptor-bound writes; every filesystem fixture is private. */
|
|
'use strict';
|
|
const assert = require('assert');
|
|
const crypto = require('crypto');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { writeFileNoFollow } = require('../../scripts/lib/install/guarded-write');
|
|
const digest = value => crypto.createHash('sha256').update(value).digest('hex');
|
|
let passed = 0;
|
|
let failed = 0;
|
|
function test(name, callback) {
|
|
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-guarded-')));
|
|
const file = path.join(root, 'entry.js');
|
|
const options = { action: 'replace activation', validateDestination: value => value };
|
|
try { callback({ root, file, options }); passed++; console.log(` PASS ${name}`); }
|
|
catch (error) { failed++; console.error(` FAIL ${name}: ${error.stack}`); }
|
|
finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
}
|
|
function replaceMethod(name, replacement, callback) {
|
|
const original = fs[name];
|
|
fs[name] = replacement(original);
|
|
try { callback(); } finally { fs[name] = original; }
|
|
}
|
|
function existing(options, content = 'old activation bytes') {
|
|
return { ...options, expectedContent: Object.freeze({ kind: 'sha256', digest: digest(content) }) };
|
|
}
|
|
test('hashing then writing shorter bytes starts at zero and preserves the requested mode', ({ file, options }) => {
|
|
fs.writeFileSync(file, 'old activation bytes');
|
|
writeFileNoFollow(file, 'off\n', { ...existing(options), mode: 0o600 });
|
|
assert.deepStrictEqual(fs.readFileSync(file), Buffer.from('off\n'));
|
|
assert.strictEqual(fs.statSync(file).size, 4);
|
|
if (process.platform !== 'win32') assert.strictEqual(fs.statSync(file).mode & 0o777, 0o600);
|
|
});
|
|
test('short writes are completed at explicit positions', ({ file, options }) => {
|
|
const positions = [];
|
|
replaceMethod('writeSync', original => (fd, buffer, offset, length, position) => {
|
|
positions.push(position);
|
|
return original(fd, buffer, offset, Math.min(length, 2), position);
|
|
}, () => writeFileNoFollow(file, 'abcdef', options));
|
|
assert.deepStrictEqual(positions, [0, 2, 4]);
|
|
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'abcdef');
|
|
});
|
|
test('expected absence creates a new exact file', ({ file, options }) => {
|
|
writeFileNoFollow(file, Buffer.from('inert'), { ...options, expectedContent: { kind: 'absent' } });
|
|
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'inert');
|
|
});
|
|
test('a competing file before exclusive open is preserved', ({ file, options }) => {
|
|
replaceMethod('openSync', original => (...args) => {
|
|
if (args[0] === file && typeof args[1] === 'number') fs.writeFileSync(file, 'user bytes');
|
|
return original(...args);
|
|
}, () => assert.throws(() => writeFileNoFollow(file, 'off', {
|
|
...options, expectedContent: { kind: 'absent' },
|
|
}), /changed after preflight/));
|
|
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'user bytes');
|
|
});
|
|
test('existing content removed before open is never recreated', ({ file, options }) => {
|
|
fs.writeFileSync(file, 'old activation bytes');
|
|
replaceMethod('openSync', original => (...args) => {
|
|
if (args[0] === file) fs.unlinkSync(file);
|
|
return original(...args);
|
|
}, () => assert.throws(() => writeFileNoFollow(file, 'off', existing(options)), /changed after preflight/));
|
|
assert.strictEqual(fs.existsSync(file), false);
|
|
});
|
|
test('same-inode edit at the writable-open boundary refuses before truncate and closes once', ({ file, options }) => {
|
|
fs.writeFileSync(file, 'old activation bytes');
|
|
let descriptor;
|
|
let closes = 0;
|
|
let truncates = 0;
|
|
const originalOpen = fs.openSync;
|
|
const originalClose = fs.closeSync;
|
|
const originalTruncate = fs.ftruncateSync;
|
|
fs.openSync = (...args) => {
|
|
const fd = originalOpen(...args);
|
|
if (args[0] === file && typeof args[1] === 'number') {
|
|
descriptor = fd;
|
|
fs.writeFileSync(file, 'user edited activation');
|
|
}
|
|
return fd;
|
|
};
|
|
fs.closeSync = fd => { if (fd === descriptor) closes++; return originalClose(fd); };
|
|
fs.ftruncateSync = (...args) => { truncates++; return originalTruncate(...args); };
|
|
try { assert.throws(() => writeFileNoFollow(file, 'off', existing(options)), /changed after preflight/); }
|
|
finally { fs.openSync = originalOpen; fs.closeSync = originalClose; fs.ftruncateSync = originalTruncate; }
|
|
assert.strictEqual(closes, 1);
|
|
assert.strictEqual(truncates, 0);
|
|
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'user edited activation');
|
|
});
|
|
test('a descriptor read concurrent edit is observed before truncation', ({ file, options }) => {
|
|
fs.writeFileSync(file, 'old activation bytes');
|
|
replaceMethod('readFileSync', original => (...args) => {
|
|
const result = original(...args);
|
|
if (typeof args[0] === 'number') fs.writeFileSync(file, 'raced during hash');
|
|
return result;
|
|
}, () => assert.throws(() => writeFileNoFollow(file, 'off', existing(options)), /changed after preflight/));
|
|
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'raced during hash');
|
|
});
|
|
test('a pathname replacement after opening preserves both original and replacement', ({ file, options }) => {
|
|
fs.writeFileSync(file, 'old activation bytes');
|
|
replaceMethod('openSync', original => (...args) => {
|
|
const fd = original(...args);
|
|
if (args[0] === file && typeof args[1] === 'number') {
|
|
fs.renameSync(file, `${file}.old`);
|
|
fs.writeFileSync(file, 'replacement');
|
|
}
|
|
return fd;
|
|
}, () => assert.throws(() => writeFileNoFollow(file, 'off', existing(options)), /changed/));
|
|
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'replacement');
|
|
assert.strictEqual(fs.readFileSync(`${file}.old`, 'utf8'), 'old activation bytes');
|
|
});
|
|
test('a directory destination is refused without writes', ({ file, options }) => {
|
|
fs.mkdirSync(file);
|
|
assert.throws(() => writeFileNoFollow(file, 'off', options));
|
|
assert.ok(fs.statSync(file).isDirectory());
|
|
});
|
|
test('a parent replacement before native open is refused even when the file identity is unchanged', ({ root, options }) => {
|
|
const parent = path.join(root, 'plugins');
|
|
fs.mkdirSync(parent);
|
|
const file = path.join(parent, 'entry.js');
|
|
const setupFd = fs.openSync(file, 'wx', 0o600);
|
|
let fileIdentity;
|
|
try {
|
|
fs.writeFileSync(setupFd, 'old activation bytes');
|
|
fileIdentity = fs.fstatSync(setupFd, { bigint: true });
|
|
} finally {
|
|
// Close before the parent swap, including on setup failure, for Windows.
|
|
fs.closeSync(setupFd);
|
|
}
|
|
const originalOpen = fs.openSync;
|
|
const originalClose = fs.closeSync;
|
|
const originalTruncate = fs.ftruncateSync;
|
|
let descriptor;
|
|
let swaps = 0;
|
|
let closes = 0;
|
|
let truncates = 0;
|
|
fs.openSync = (...args) => {
|
|
if (args[0] === file && typeof args[1] === 'number') {
|
|
// The writer has pinned the parent, but has not opened the file yet.
|
|
// Moving an open file's parent is not portable to Windows. Keep the
|
|
// same file inode and bytes so only the parent identity rejects this.
|
|
fs.renameSync(parent, `${parent}.old`);
|
|
fs.mkdirSync(parent);
|
|
fs.renameSync(path.join(`${parent}.old`, 'entry.js'), file);
|
|
swaps++;
|
|
descriptor = originalOpen(...args);
|
|
return descriptor;
|
|
}
|
|
return originalOpen(...args);
|
|
};
|
|
fs.closeSync = fd => { if (fd === descriptor) closes++; return originalClose(fd); };
|
|
fs.ftruncateSync = (...args) => { truncates++; return originalTruncate(...args); };
|
|
try {
|
|
assert.throws(() => writeFileNoFollow(file, 'off', existing(options)), error => {
|
|
assert.match(error.message, /changed after preflight/);
|
|
assert.strictEqual(error.cause, undefined, 'the parent guard, not an open error, refuses');
|
|
return true;
|
|
});
|
|
} finally {
|
|
fs.openSync = originalOpen;
|
|
fs.closeSync = originalClose;
|
|
fs.ftruncateSync = originalTruncate;
|
|
}
|
|
assert.strictEqual(swaps, 1);
|
|
assert.strictEqual(typeof descriptor, 'number');
|
|
assert.strictEqual(closes, 1);
|
|
assert.strictEqual(truncates, 0);
|
|
const postconditionFd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
try {
|
|
const replacementIdentity = fs.fstatSync(postconditionFd, { bigint: true });
|
|
assert.ok(replacementIdentity.isFile());
|
|
assert.strictEqual(replacementIdentity.dev, fileIdentity.dev);
|
|
assert.strictEqual(replacementIdentity.ino, fileIdentity.ino);
|
|
assert.strictEqual(fs.readFileSync(postconditionFd, 'utf8'), 'old activation bytes');
|
|
} finally {
|
|
fs.closeSync(postconditionFd);
|
|
}
|
|
assert.ok(fs.statSync(`${parent}.old`).isDirectory());
|
|
});
|
|
test('a denied native open preserves its cause without closing an unallocated descriptor', ({ file, options }) => {
|
|
fs.writeFileSync(file, 'old activation bytes');
|
|
const denied = Object.assign(new Error('fixture open denied'), { code: 'EPERM' });
|
|
const originalClose = fs.closeSync;
|
|
const originalTruncate = fs.ftruncateSync;
|
|
let closes = 0;
|
|
let truncates = 0;
|
|
fs.closeSync = fd => { closes++; return originalClose(fd); };
|
|
fs.ftruncateSync = (...args) => { truncates++; return originalTruncate(...args); };
|
|
try {
|
|
replaceMethod('openSync', original => (...args) => {
|
|
if (args[0] === file && typeof args[1] === 'number') throw denied;
|
|
return original(...args);
|
|
}, () => assert.throws(() => writeFileNoFollow(file, 'off', existing(options)), error => {
|
|
assert.strictEqual(error.cause, denied);
|
|
assert.strictEqual(error.code, 'EPERM');
|
|
return true;
|
|
}));
|
|
} finally {
|
|
fs.closeSync = originalClose;
|
|
fs.ftruncateSync = originalTruncate;
|
|
}
|
|
assert.strictEqual(closes, 0);
|
|
assert.strictEqual(truncates, 0);
|
|
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'old activation bytes');
|
|
});
|
|
test('destination validator is mandatory and must return the same path', ({ file, options }) => {
|
|
assert.throws(() => writeFileNoFollow(file, 'off', {}), /validateDestination/);
|
|
assert.throws(() => writeFileNoFollow(file, 'off', { ...options, validateDestination: () => `${file}.other` }), /changed/);
|
|
assert.strictEqual(fs.existsSync(file), false);
|
|
});
|
|
test('destination validation runs again after the descriptor hash', ({ file, options }) => {
|
|
fs.writeFileSync(file, 'old activation bytes');
|
|
let validations = 0;
|
|
assert.throws(() => writeFileNoFollow(file, 'off', {
|
|
...existing(options), validateDestination: value => {
|
|
validations++;
|
|
if (validations === 3) throw new Error('containment changed');
|
|
return value;
|
|
},
|
|
}), /containment changed/);
|
|
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'old activation bytes');
|
|
});
|
|
for (const method of ['readFileSync', 'ftruncateSync', 'writeSync', 'fchmodSync']) {
|
|
test(`${method} failure closes once and preserves the primary error if close also fails`, ({ file, options }) => {
|
|
fs.writeFileSync(file, 'old activation bytes');
|
|
const primary = new Error(`${method} primary`);
|
|
const secondary = new Error('close secondary');
|
|
const original = fs[method];
|
|
const close = fs.closeSync;
|
|
let closes = 0;
|
|
fs[method] = (...args) => { if (typeof args[0] === 'number') throw primary; return original(...args); };
|
|
fs.closeSync = fd => { closes++; close(fd); throw secondary; };
|
|
try {
|
|
assert.throws(() => writeFileNoFollow(file, 'off', { ...existing(options), mode: 0o600 }), error => {
|
|
assert.strictEqual(error, primary);
|
|
assert.strictEqual(error.closeError, secondary);
|
|
return true;
|
|
});
|
|
} finally { fs[method] = original; fs.closeSync = close; }
|
|
assert.strictEqual(closes, 1);
|
|
});
|
|
}
|
|
test('close failure after a successful write is reported', ({ file, options }) => {
|
|
replaceMethod('closeSync', original => fd => { original(fd); throw new Error('close failed'); }, () => {
|
|
assert.throws(() => writeFileNoFollow(file, 'off', options), /close failed/);
|
|
});
|
|
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'off');
|
|
});
|
|
test('zero-progress writes refuse instead of spinning', ({ file, options }) => {
|
|
replaceMethod('writeSync', () => () => 0, () => assert.throws(() => writeFileNoFollow(file, 'off', options), /progress/));
|
|
});
|
|
test('malformed expected-content guards fail before opening a destination', ({ file, options }) => {
|
|
for (const expectedContent of [null, true, { kind: 'sha256', digest: 'bad' }, { kind: 'missing' }]) {
|
|
assert.throws(() => writeFileNoFollow(file, 'off', { ...options, expectedContent }), /expectedContent/);
|
|
}
|
|
assert.strictEqual(fs.existsSync(file), false);
|
|
});
|
|
test('a new exclusive file changed through the owned descriptor is preserved', ({ file, options }) => {
|
|
replaceMethod('openSync', original => (...args) => {
|
|
const fd = original(...args);
|
|
if (args[0] === file && typeof args[1] === 'number') fs.writeSync(fd, Buffer.from('raced creation'), 0, 14, 0);
|
|
return fd;
|
|
}, () => assert.throws(() => writeFileNoFollow(file, 'off', {
|
|
...options, expectedContent: { kind: 'absent' },
|
|
}), /changed after preflight/));
|
|
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'raced creation');
|
|
});
|
|
test('the final pathname must remain a regular non-symlink even without O_NOFOLLOW support', ({ file, options }) => {
|
|
fs.writeFileSync(file, 'old activation bytes');
|
|
replaceMethod('lstatSync', original => (...args) => {
|
|
const stats = original(...args);
|
|
if (args[0] === file) {
|
|
const simulatedSymlink = Object.create(stats);
|
|
simulatedSymlink.isSymbolicLink = () => true;
|
|
return simulatedSymlink;
|
|
}
|
|
return stats;
|
|
}, () => assert.throws(() => writeFileNoFollow(file, 'off', existing(options)), /changed/));
|
|
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'old activation bytes');
|
|
});
|
|
test('the immutable preflight digest cannot be changed by a validator callback', ({ file, options }) => {
|
|
fs.writeFileSync(file, 'user bytes');
|
|
const expectedContent = { kind: 'sha256', digest: digest('old bytes') };
|
|
assert.throws(() => writeFileNoFollow(file, 'off', {
|
|
...options, expectedContent, validateDestination: value => {
|
|
expectedContent.digest = digest('user bytes');
|
|
return value;
|
|
},
|
|
}), /changed after preflight/);
|
|
assert.strictEqual(fs.readFileSync(file, 'utf8'), 'user bytes');
|
|
});
|
|
test('unexpected input fails without creating a file', ({ file, options }) => {
|
|
assert.throws(() => writeFileNoFollow(file, {}, options), /content/);
|
|
assert.strictEqual(fs.existsSync(file), false);
|
|
});
|
|
console.log(`Results: Passed: ${passed}, Failed: ${failed}`);
|
|
process.exitCode = failed ? 1 : 0;
|