- open_ctx takes a non-blocking flock on the journal. A journal locked by another kte process is left alone: this session reports it once and does not write (records from two sessions used to interleave in one file). Opening a file whose journal is live in another session skips recovery instead of replaying or removing that journal. - The header records the size and mtime of the file the journal's records apply to (flags bit 0, bytes 24..39; previously reserved, so no version change). Before offering recovery, a journal whose base no longer matches the file on disk (changed after a crash, or the crash fell between a save's rename and the journal reset) is treated as stale and offered for deletion, instead of replaying its edits onto different text. Journals from older kte (flag clear) replay as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
244 lines
6.9 KiB
C++
244 lines
6.9 KiB
C++
#include "Test.h"
|
|
|
|
#include "Buffer.h"
|
|
#include "Swap.h"
|
|
|
|
#include <cstdint>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <cstdlib>
|
|
#include <cstdio>
|
|
#include <unistd.h>
|
|
|
|
#include <sys/stat.h>
|
|
|
|
namespace {
|
|
std::vector<std::uint8_t>
|
|
read_all_bytes(const std::string &path)
|
|
{
|
|
std::ifstream in(path, std::ios::binary);
|
|
return std::vector<std::uint8_t>((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
|
}
|
|
|
|
|
|
std::uint32_t
|
|
read_le32(const std::uint8_t *p)
|
|
{
|
|
return (std::uint32_t) p[0] | ((std::uint32_t) p[1] << 8) | ((std::uint32_t) p[2] << 16) |
|
|
((std::uint32_t) p[3] << 24);
|
|
}
|
|
|
|
|
|
std::uint64_t
|
|
read_le64(const std::uint8_t *p)
|
|
{
|
|
std::uint64_t v = 0;
|
|
for (int i = 7; i >= 0; --i) {
|
|
v = (v << 8) | p[i];
|
|
}
|
|
return v;
|
|
}
|
|
|
|
|
|
std::uint32_t
|
|
crc32(const std::uint8_t *data, std::size_t len, std::uint32_t seed = 0)
|
|
{
|
|
static std::uint32_t table[256];
|
|
static bool inited = false;
|
|
if (!inited) {
|
|
for (std::uint32_t i = 0; i < 256; ++i) {
|
|
std::uint32_t c = i;
|
|
for (int j = 0; j < 8; ++j)
|
|
c = (c & 1) ? (0xEDB88320u ^ (c >> 1)) : (c >> 1);
|
|
table[i] = c;
|
|
}
|
|
inited = true;
|
|
}
|
|
std::uint32_t c = ~seed;
|
|
for (std::size_t i = 0; i < len; ++i)
|
|
c = table[(c ^ data[i]) & 0xFFu] ^ (c >> 8);
|
|
return ~c;
|
|
}
|
|
} // namespace
|
|
|
|
|
|
TEST(SwapWriter_Header_Records_And_CRC)
|
|
{
|
|
const std::filesystem::path xdg_root = std::filesystem::temp_directory_path() /
|
|
(std::string("kte_ut_xdg_state_") + std::to_string((int) ::getpid()));
|
|
std::filesystem::remove_all(xdg_root);
|
|
|
|
const char *old_xdg_p = std::getenv("XDG_STATE_HOME");
|
|
const std::string old_xdg = old_xdg_p ? std::string(old_xdg_p) : std::string();
|
|
const std::string xdg_root_s = xdg_root.string();
|
|
setenv("XDG_STATE_HOME", xdg_root_s.c_str(), 1);
|
|
|
|
const std::string path = (xdg_root / "work" / "kte_ut_swap_writer.txt").string();
|
|
std::filesystem::create_directories((xdg_root / "work"));
|
|
|
|
// Clean up from prior runs
|
|
std::remove(path.c_str());
|
|
|
|
// Ensure file exists so buffer is file-backed
|
|
{
|
|
std::ofstream out(path, std::ios::binary);
|
|
out << "";
|
|
}
|
|
|
|
Buffer b;
|
|
std::string err;
|
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
|
ASSERT_TRUE(err.empty());
|
|
ASSERT_TRUE(b.IsFileBacked());
|
|
|
|
kte::SwapManager sm;
|
|
sm.Attach(&b);
|
|
b.SetSwapRecorder(sm.RecorderFor(&b));
|
|
const std::string swp = kte::SwapManager::ComputeSwapPathForTests(b);
|
|
std::remove(swp.c_str());
|
|
|
|
// Emit one INS and one DEL
|
|
b.insert_text(0, 0, std::string_view("abc"));
|
|
b.delete_text(0, 1, 1);
|
|
|
|
// Ensure all records are written before reading
|
|
sm.Flush(&b);
|
|
sm.Detach(&b);
|
|
b.SetSwapRecorder(nullptr);
|
|
|
|
ASSERT_TRUE(std::filesystem::exists(swp));
|
|
|
|
// Verify permissions 0600
|
|
struct stat st{};
|
|
ASSERT_TRUE(::stat(swp.c_str(), &st) == 0);
|
|
ASSERT_EQ((st.st_mode & 0777), 0600);
|
|
|
|
const std::vector<std::uint8_t> bytes = read_all_bytes(swp);
|
|
ASSERT_TRUE(bytes.size() >= 64);
|
|
|
|
// Header
|
|
static const std::uint8_t magic[8] = {'K', 'T', 'E', '_', 'S', 'W', 'P', '\0'};
|
|
for (int i = 0; i < 8; ++i)
|
|
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);
|
|
ASSERT_EQ(read_le64(bytes.data() + 24), (std::uint64_t) std::filesystem::file_size(b.Filename()));
|
|
ASSERT_TRUE(read_le64(bytes.data() + 16) != 0);
|
|
|
|
// Records
|
|
std::vector<std::uint8_t> types;
|
|
std::size_t off = 64;
|
|
while (off < bytes.size()) {
|
|
ASSERT_TRUE(bytes.size() - off >= 8); // at least header+crc
|
|
const std::uint8_t type = bytes[off + 0];
|
|
const std::uint32_t len = (std::uint32_t) bytes[off + 1] | ((std::uint32_t) bytes[off + 2] << 8) |
|
|
((std::uint32_t) bytes[off + 3] << 16);
|
|
const std::size_t payload_off = off + 4;
|
|
const std::size_t crc_off = payload_off + len;
|
|
ASSERT_TRUE(crc_off + 4 <= bytes.size());
|
|
|
|
const std::uint32_t got_crc = read_le32(bytes.data() + crc_off);
|
|
std::uint32_t c = 0;
|
|
c = crc32(bytes.data() + off, 4, c);
|
|
c = crc32(bytes.data() + payload_off, len, c);
|
|
ASSERT_EQ(got_crc, c);
|
|
|
|
types.push_back(type);
|
|
off = crc_off + 4;
|
|
}
|
|
|
|
ASSERT_EQ(types.size(), (std::size_t) 3);
|
|
ASSERT_EQ(types[0], (std::uint8_t) kte::SwapRecType::INS);
|
|
ASSERT_EQ(types[1], (std::uint8_t) kte::SwapRecType::DEL);
|
|
ASSERT_EQ(types[2], (std::uint8_t) kte::SwapRecType::CHKPT);
|
|
|
|
std::remove(path.c_str());
|
|
std::remove(swp.c_str());
|
|
if (!old_xdg.empty()) {
|
|
setenv("XDG_STATE_HOME", old_xdg.c_str(), 1);
|
|
} else {
|
|
unsetenv("XDG_STATE_HOME");
|
|
}
|
|
std::filesystem::remove_all(xdg_root);
|
|
}
|
|
|
|
|
|
TEST(SwapWriter_NoStomp_SameBasename)
|
|
{
|
|
const std::filesystem::path xdg_root = std::filesystem::temp_directory_path() /
|
|
(std::string("kte_ut_xdg_state_nostomp_") + std::to_string(
|
|
(int) ::getpid()));
|
|
std::filesystem::remove_all(xdg_root);
|
|
std::filesystem::create_directories(xdg_root);
|
|
|
|
const char *old_xdg_p = std::getenv("XDG_STATE_HOME");
|
|
const std::string old_xdg = old_xdg_p ? std::string(old_xdg_p) : std::string();
|
|
const std::string xdg_root_s = xdg_root.string();
|
|
setenv("XDG_STATE_HOME", xdg_root_s.c_str(), 1);
|
|
|
|
const std::filesystem::path d1 = xdg_root / "p1";
|
|
const std::filesystem::path d2 = xdg_root / "p2";
|
|
std::filesystem::create_directories(d1);
|
|
std::filesystem::create_directories(d2);
|
|
const std::filesystem::path f1 = d1 / "same.txt";
|
|
const std::filesystem::path f2 = d2 / "same.txt";
|
|
|
|
{
|
|
std::ofstream out(f1.string(), std::ios::binary);
|
|
out << "";
|
|
}
|
|
{
|
|
std::ofstream out(f2.string(), std::ios::binary);
|
|
out << "";
|
|
}
|
|
|
|
Buffer b1;
|
|
Buffer b2;
|
|
std::string err;
|
|
ASSERT_TRUE(b1.OpenFromFile(f1.string(), err));
|
|
ASSERT_TRUE(err.empty());
|
|
ASSERT_TRUE(b2.OpenFromFile(f2.string(), err));
|
|
ASSERT_TRUE(err.empty());
|
|
|
|
const std::string swp1 = kte::SwapManager::ComputeSwapPathForTests(b1);
|
|
const std::string swp2 = kte::SwapManager::ComputeSwapPathForTests(b2);
|
|
ASSERT_TRUE(swp1 != swp2);
|
|
|
|
// Actually write to both to ensure one doesn't clobber the other.
|
|
kte::SwapManager sm;
|
|
sm.Attach(&b1);
|
|
sm.Attach(&b2);
|
|
b1.SetSwapRecorder(sm.RecorderFor(&b1));
|
|
b2.SetSwapRecorder(sm.RecorderFor(&b2));
|
|
|
|
b1.insert_text(0, 0, std::string_view("one"));
|
|
b2.insert_text(0, 0, std::string_view("two"));
|
|
sm.Flush();
|
|
|
|
ASSERT_TRUE(std::filesystem::exists(swp1));
|
|
ASSERT_TRUE(std::filesystem::exists(swp2));
|
|
ASSERT_TRUE(std::filesystem::file_size(swp1) >= 64);
|
|
ASSERT_TRUE(std::filesystem::file_size(swp2) >= 64);
|
|
|
|
sm.Detach(&b1);
|
|
sm.Detach(&b2);
|
|
b1.SetSwapRecorder(nullptr);
|
|
b2.SetSwapRecorder(nullptr);
|
|
|
|
std::remove(swp1.c_str());
|
|
std::remove(swp2.c_str());
|
|
std::remove(f1.string().c_str());
|
|
std::remove(f2.string().c_str());
|
|
if (!old_xdg.empty()) {
|
|
setenv("XDG_STATE_HOME", old_xdg.c_str(), 1);
|
|
} else {
|
|
unsetenv("XDG_STATE_HOME");
|
|
}
|
|
std::filesystem::remove_all(xdg_root);
|
|
}
|