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:
@@ -974,3 +974,21 @@ TEST(Audit_SaveAs_FileOpenInOtherBuffer_Refused)
|
||||
ASSERT_EQ(ed.BufferCount(), n);
|
||||
ASSERT_EQ(ed.FindOpenBuffer((d.path / "y.txt").string()) != ed.CurrentBufferIndex(), true);
|
||||
}
|
||||
|
||||
|
||||
// Saving keeps setuid/setgid bits (chown after chmod used to clear them).
|
||||
TEST(Audit_Save_KeepsSpecialModeBits)
|
||||
{
|
||||
TempDir d("save_setuid");
|
||||
const auto f = d.path / "s.sh";
|
||||
std::ofstream(f) << "echo\n";
|
||||
ASSERT_EQ(::chmod(f.c_str(), 06755), 0);
|
||||
Buffer b;
|
||||
std::string err;
|
||||
ASSERT_TRUE(b.OpenFromFile(f.string(), err));
|
||||
b.insert_text(0, 0, "#");
|
||||
ASSERT_TRUE(b.Save(err));
|
||||
struct stat st{};
|
||||
ASSERT_EQ(::stat(f.c_str(), &st), 0);
|
||||
ASSERT_EQ(st.st_mode & 07777, (mode_t) 06755);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -125,8 +125,8 @@ TEST(SwapWriter_Header_Records_And_CRC)
|
||||
ASSERT_EQ(bytes[(std::size_t) i], magic[i]);
|
||||
ASSERT_EQ(read_le32(bytes.data() + 8), (std::uint32_t) 1);
|
||||
// flags bit 0: the base file's identity (size, mtime) is recorded, since
|
||||
// the buffer is file-backed.
|
||||
ASSERT_EQ(read_le32(bytes.data() + 12), (std::uint32_t) 1);
|
||||
// the buffer is file-backed; bit 1: its content CRC-32 too.
|
||||
ASSERT_EQ(read_le32(bytes.data() + 12), (std::uint32_t) 3);
|
||||
ASSERT_EQ(read_le64(bytes.data() + 24), (std::uint64_t) std::filesystem::file_size(b.Filename()));
|
||||
ASSERT_TRUE(read_le64(bytes.data() + 16) != 0);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user