Save metadata and journal staleness: second-pass fixes

- Ownership is applied before permissions: chown clears setuid/setgid,
  so saving a 06755 file left it 0755.
- The umask is read once during static initialisation. Reading it at
  save time needed umask(0)/umask(old), briefly changing it for the swap
  writer thread creating files.
- Before rewriting a hard-linked file in place, the new content is
  written and synced to a copy beside it; if the in-place write fails
  (ENOSPC, EIO) the copy is kept and named in the error.
- Journal headers also record a CRC-32 of the base file (flags bit 1,
  bytes 40..43). A changed mtime with the same size and content
  (touch, checkout back and forth) no longer marks a journal stale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
This commit is contained in:
Claude
2026-09-24 09:16:04 -07:00
committed by Kyle Isom
parent 6c2727789e
commit 1005f5dbe8
6 changed files with 162 additions and 14 deletions
+26
View File
@@ -7,6 +7,7 @@
#include "tests/TestHarness.h" // for ktet::InstallDefaultCommandsOnce
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
@@ -653,3 +654,28 @@ TEST(SwapRecoveryPrompt_EmptyAnswer_KeepsSwap)
ASSERT_TRUE(!ed.PromptActive());
ASSERT_TRUE(std::filesystem::exists(swp));
}
// A timestamp-only change (touch, git checkout back and forth) does not make
// a journal stale: its base content is unchanged.
TEST(SwapRecoveryPrompt_TouchedFile_StillRecoverable)
{
ktet::InstallDefaultCommandsOnce();
XdgSandbox sb("touched");
const std::string file = (sb.root / "work" / "t2.txt").string();
write_file_bytes(file, "base\n");
std::string expected;
(void) make_journal(file, [](Buffer &b) {
b.insert_text(0, 0, std::string("A"));
}, expected);
std::filesystem::last_write_time(file, std::filesystem::last_write_time(file) + std::chrono::hours(1));
Editor ed;
ed.SetDimensions(24, 80);
ed.AddBuffer(Buffer());
ed.RequestOpenFile(file);
(void) ed.ProcessPendingOpens();
ASSERT_EQ(ed.PendingRecoveryPrompt(), Editor::RecoveryPromptKind::RecoverOrDiscard);
answer(ed, "y");
ASSERT_EQ(buffer_bytes_via_views(*ed.CurrentBuffer()), expected);
}