fix: make saveAliases atomic on Unix by skipping unnecessary unlink before rename

On Unix/macOS, rename(2) atomically replaces the destination file.
The previous code ran unlinkSync before renameSync on all platforms,
creating an unnecessary non-atomic window where a crash could lose
data. Now the delete-before-rename is gated behind process.platform
=== 'win32', where rename cannot overwrite an existing file.
This commit is contained in:
Affaan Mustafa
2026-02-13 04:23:22 -08:00
parent 0daa5cb070
commit dc11fc2fd8
2 changed files with 29 additions and 2 deletions

View File

@@ -110,8 +110,10 @@ function saveAliases(aliases) {
// Atomic write: write to temp file, then rename
fs.writeFileSync(tempPath, content, 'utf8');
// On Windows, we need to delete the target file before renaming
if (fs.existsSync(aliasesPath)) {
// On Windows, rename fails with EEXIST if destination exists, so delete first.
// On Unix/macOS, rename(2) atomically replaces the destination — skip the
// delete to avoid an unnecessary non-atomic window between unlink and rename.
if (process.platform === 'win32' && fs.existsSync(aliasesPath)) {
fs.unlinkSync(aliasesPath);
}
fs.renameSync(tempPath, aliasesPath);

View File

@@ -801,6 +801,31 @@ function runTests() {
'Original aliases data should be preserved after failed save');
})) passed++; else failed++;
// ── Round 39: atomic overwrite on Unix (no unlink before rename) ──
console.log('\nRound 39: atomic overwrite:');
if (test('saveAliases overwrites existing file atomically', () => {
// Create initial aliases
aliases.setAlias('atomic-test', '2026-01-01-abc123-session.tmp');
const aliasesPath = aliases.getAliasesPath();
assert.ok(fs.existsSync(aliasesPath), 'Aliases file should exist');
const sizeBefore = fs.statSync(aliasesPath).size;
assert.ok(sizeBefore > 0, 'Aliases file should have content');
// Overwrite with different data
aliases.setAlias('atomic-test-2', '2026-02-01-def456-session.tmp');
// The file should still exist and be valid JSON
const content = fs.readFileSync(aliasesPath, 'utf8');
const parsed = JSON.parse(content);
assert.ok(parsed.aliases['atomic-test'], 'First alias should exist');
assert.ok(parsed.aliases['atomic-test-2'], 'Second alias should exist');
// Cleanup
aliases.deleteAlias('atomic-test');
aliases.deleteAlias('atomic-test-2');
})) passed++; else failed++;
// Cleanup — restore both HOME and USERPROFILE (Windows)
process.env.HOME = origHome;
if (origUserProfile !== undefined) {