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
+53 -4
View File
@@ -354,7 +354,7 @@ SwapManager::Attach(Buffer *buf)
if (ctx.path.empty())
ctx.path = ComputeSidecarPath(*buf);
if (fresh)
ctx.has_base = stat_base(buf->Filename(), ctx.base_size, ctx.base_mtime_ns);
capture_base(buf->Filename(), ctx);
// Ensure a recorder exists as well.
if (recorders_.find(buf) == recorders_.end()) {
recorders_[buf] = std::make_unique<BufferRecorder>(*this, *buf);
@@ -433,7 +433,7 @@ SwapManager::ResetJournal(Buffer &buf)
ctx.gap_chkpt_request_ns = 0;
ctx.gap_unfixable_reported = false;
ctx.locked_out = false;
ctx.has_base = stat_base(buf.Filename(), ctx.base_size, ctx.base_mtime_ns);
capture_base(buf.Filename(), ctx);
ctx.last_flush_ns = 0;
ctx.last_fsync_ns = 0;
ctx.last_chkpt_ns = 0;
@@ -566,7 +566,7 @@ SwapManager::NotifyFilenameChanged(Buffer &buf)
ctx.gap_chkpt_request_ns = 0;
ctx.gap_unfixable_reported = false;
ctx.locked_out = false;
ctx.has_base = stat_base(buf.Filename(), ctx.base_size, ctx.base_mtime_ns);
capture_base(buf.Filename(), ctx);
ctx.path = new_path;
ctx.suspended = false;
ctx.header_ok = false;
@@ -693,10 +693,18 @@ SwapManager::write_header(int fd, const JournalCtx &ctx)
hdr[10] = static_cast<std::uint8_t>((VERSION >> 16) & 0xFFu);
hdr[11] = static_cast<std::uint8_t>((VERSION >> 24) & 0xFFu);
// flags: bit 0 = base file identity present
// flags: bit 1 = base content CRC-32 present (bytes 40..43)
if (ctx.has_base) {
hdr[12] = 1;
put_le64(hdr + 24, ctx.base_size);
put_le64(hdr + 32, static_cast<std::uint64_t>(ctx.base_mtime_ns));
if (ctx.has_base_crc) {
hdr[12] |= 2;
hdr[40] = static_cast<std::uint8_t>(ctx.base_crc & 0xFFu);
hdr[41] = static_cast<std::uint8_t>((ctx.base_crc >> 8) & 0xFFu);
hdr[42] = static_cast<std::uint8_t>((ctx.base_crc >> 16) & 0xFFu);
hdr[43] = static_cast<std::uint8_t>((ctx.base_crc >> 24) & 0xFFu);
}
}
// created_time (unix seconds; little-endian)
std::uint64_t ts = static_cast<std::uint64_t>(std::time(nullptr));
@@ -1515,7 +1523,48 @@ SwapManager::JournalMatchesFile(const std::string &swap_path, const std::string
std::int64_t mtime = 0;
if (!stat_base(file_path, size, mtime))
return false; // the file the journal was based on is gone
return size == want_size && static_cast<std::uint64_t>(mtime) == want_mtime;
if (size != want_size)
return false;
if (static_cast<std::uint64_t>(mtime) == want_mtime)
return true;
// Timestamp changed (touch, checkout back and forth, sync tools): the
// content may still be the same, which is what matters.
if ((hdr[12] & 2u) == 0)
return false;
const std::uint32_t want_crc = static_cast<std::uint32_t>(hdr[40]) | (static_cast<std::uint32_t>(hdr[41]) << 8) |
(static_cast<std::uint32_t>(hdr[42]) << 16) |
(static_cast<std::uint32_t>(hdr[43]) << 24);
std::uint32_t crc = 0;
return file_crc32(file_path, crc) && crc == want_crc;
}
bool
SwapManager::file_crc32(const std::string &path, std::uint32_t &out)
{
std::ifstream in(path, std::ios::binary);
if (!in)
return false;
std::uint32_t c = 0;
std::uint8_t chunk[1 << 16];
while (in) {
in.read(reinterpret_cast<char *>(chunk), sizeof(chunk));
const auto n = in.gcount();
if (n > 0)
c = crc32(chunk, static_cast<std::size_t>(n), c);
}
if (in.bad())
return false;
out = c;
return true;
}
void
SwapManager::capture_base(const std::string &file, JournalCtx &ctx)
{
ctx.has_base = stat_base(file, ctx.base_size, ctx.base_mtime_ns);
ctx.has_base_crc = ctx.has_base && file_crc32(file, ctx.base_crc);
}