diff --git a/Buffer.cc b/Buffer.cc index 313309b..2d69e11 100644 --- a/Buffer.cc +++ b/Buffer.cc @@ -417,11 +417,34 @@ Buffer::OpenFromFile(const std::string &path, std::string &err) // Read entire file into PieceTable as-is std::string data; in.seekg(0, std::ios::end); + if (!in) { + err = "Failed to seek to end of file: " + norm; + return false; + } auto sz = in.tellg(); + if (sz < 0) { + err = "Failed to get file size: " + norm; + return false; + } if (sz > 0) { data.resize(static_cast(sz)); in.seekg(0, std::ios::beg); + if (!in) { + err = "Failed to seek to beginning of file: " + norm; + return false; + } in.read(data.data(), static_cast(data.size())); + if (!in && !in.eof()) { + err = "Failed to read file: " + norm; + return false; + } + // Validate we read the expected number of bytes + const std::streamsize bytes_read = in.gcount(); + if (bytes_read != static_cast(data.size())) { + err = "Partial read of file (expected " + std::to_string(data.size()) + + " bytes, got " + std::to_string(bytes_read) + "): " + norm; + return false; + } } content_.Clear(); if (!data.empty()) @@ -760,4 +783,4 @@ const UndoSystem * Buffer::Undo() const { return undo_sys_.get(); -} +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 0b54631..401604b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -316,6 +316,7 @@ if (BUILD_TESTS) tests/test_swap_recorder.cc tests/test_swap_writer.cc tests/test_swap_replay.cc + tests/test_swap_edge_cases.cc tests/test_swap_recovery_prompt.cc tests/test_swap_cleanup.cc tests/test_swap_git_editor.cc diff --git a/Swap.cc b/Swap.cc index 4f54242..6da2e6c 100644 --- a/Swap.cc +++ b/Swap.cc @@ -598,24 +598,32 @@ SwapManager::write_header(int fd) bool -SwapManager::open_ctx(JournalCtx &ctx, const std::string &path) +SwapManager::open_ctx(JournalCtx &ctx, const std::string &path, std::string &err) { + err.clear(); if (ctx.fd >= 0) return true; - if (!ensure_parent_dir(path)) + if (!ensure_parent_dir(path)) { + err = "Failed to create parent directory for swap file: " + path; return false; + } int flags = O_CREAT | O_WRONLY | O_APPEND; #ifdef O_CLOEXEC flags |= O_CLOEXEC; #endif int fd = ::open(path.c_str(), flags, 0600); - if (fd < 0) + if (fd < 0) { + int saved_errno = errno; + err = "Failed to open swap file '" + path + "': " + std::strerror(saved_errno); return false; + } // Ensure permissions even if file already existed. (void) ::fchmod(fd, 0600); struct stat st{}; if (fstat(fd, &st) != 0) { + int saved_errno = errno; ::close(fd); + err = "Failed to fstat swap file '" + path + "': " + std::strerror(saved_errno); return false; } // If an existing file is too small to contain the fixed header, truncate @@ -627,8 +635,11 @@ SwapManager::open_ctx(JournalCtx &ctx, const std::string &path) tflags |= O_CLOEXEC; #endif fd = ::open(path.c_str(), tflags, 0600); - if (fd < 0) + if (fd < 0) { + int saved_errno = errno; + err = "Failed to reopen swap file for truncation '" + path + "': " + std::strerror(saved_errno); return false; + } (void) ::fchmod(fd, 0600); st.st_size = 0; } @@ -637,6 +648,9 @@ SwapManager::open_ctx(JournalCtx &ctx, const std::string &path) if (st.st_size == 0) { ctx.header_ok = write_header(fd); ctx.approx_size_bytes = ctx.header_ok ? 64 : 0; + if (!ctx.header_ok) { + err = "Failed to write swap file header: " + path; + } } else { ctx.header_ok = true; // stage 1: trust existing header ctx.approx_size_bytes = static_cast(st.st_size); @@ -658,12 +672,17 @@ SwapManager::close_ctx(JournalCtx &ctx) bool -SwapManager::compact_to_checkpoint(JournalCtx &ctx, const std::vector &chkpt_record) +SwapManager::compact_to_checkpoint(JournalCtx &ctx, const std::vector &chkpt_record, std::string &err) { - if (ctx.path.empty()) + err.clear(); + if (ctx.path.empty()) { + err = "Compact failed: empty path"; return false; - if (chkpt_record.empty()) + } + if (chkpt_record.empty()) { + err = "Compact failed: empty checkpoint record"; return false; + } // Close existing file before rename. if (ctx.fd >= 0) { @@ -675,30 +694,46 @@ SwapManager::compact_to_checkpoint(JournalCtx &ctx, const std::vector(chkpt_record.size()); return true; } @@ -969,7 +1006,13 @@ SwapManager::writer_loop() continue; for (const Pending &p: batch) { - process_one(p); + try { + process_one(p); + } catch (const std::exception &e) { + report_error(std::string("Exception in process_one: ") + e.what(), p.buf); + } catch (...) { + report_error("Unknown exception in process_one", p.buf); + } { std::lock_guard lg(mtx_); if (p.seq > last_processed_) @@ -981,23 +1024,29 @@ SwapManager::writer_loop() } // Throttled fsync: best-effort (grouped) - std::vector to_sync; - std::uint64_t now = now_ns(); - { - std::lock_guard lg(mtx_); - for (auto &kv: journals_) { - JournalCtx &ctx = kv.second; - if (ctx.fd >= 0) { - if (ctx.last_fsync_ns == 0 || (now - ctx.last_fsync_ns) / 1000000ULL >= - cfg_.fsync_interval_ms) { - ctx.last_fsync_ns = now; - to_sync.push_back(ctx.fd); + try { + std::vector to_sync; + std::uint64_t now = now_ns(); + { + std::lock_guard lg(mtx_); + for (auto &kv: journals_) { + JournalCtx &ctx = kv.second; + if (ctx.fd >= 0) { + if (ctx.last_fsync_ns == 0 || (now - ctx.last_fsync_ns) / 1000000ULL >= + cfg_.fsync_interval_ms) { + ctx.last_fsync_ns = now; + to_sync.push_back(ctx.fd); + } } } } - } - for (int fd: to_sync) { - (void) ::fsync(fd); + for (int fd: to_sync) { + (void) ::fsync(fd); + } + } catch (const std::exception &e) { + report_error(std::string("Exception in fsync operations: ") + e.what()); + } catch (...) { + report_error("Unknown exception in fsync operations"); } } // Wake any waiters. @@ -1010,70 +1059,90 @@ SwapManager::process_one(const Pending &p) { if (!p.buf) return; - Buffer &buf = *p.buf; - JournalCtx *ctxp = nullptr; - std::string path; - std::size_t compact_bytes = 0; - { - std::lock_guard lg(mtx_); - auto it = journals_.find(p.buf); - if (it == journals_.end()) + try { + Buffer &buf = *p.buf; + + JournalCtx *ctxp = nullptr; + std::string path; + std::size_t compact_bytes = 0; + { + std::lock_guard lg(mtx_); + auto it = journals_.find(p.buf); + if (it == journals_.end()) + return; + if (it->second.path.empty()) + it->second.path = ComputeSidecarPath(buf); + path = it->second.path; + ctxp = &it->second; + compact_bytes = cfg_.compact_bytes; + } + if (!ctxp) return; - if (it->second.path.empty()) - it->second.path = ComputeSidecarPath(buf); - path = it->second.path; - ctxp = &it->second; - compact_bytes = cfg_.compact_bytes; - } - if (!ctxp) - return; - if (!open_ctx(*ctxp, path)) - return; - if (p.payload.size() > 0xFFFFFFu) - return; + std::string open_err; + if (!open_ctx(*ctxp, path, open_err)) { + report_error(open_err, p.buf); + return; + } + if (p.payload.size() > 0xFFFFFFu) { + report_error("Payload too large: " + std::to_string(p.payload.size()) + " bytes", p.buf); + return; + } - // Build record: [type u8][len u24][payload][crc32 u32] - std::uint8_t len3[3]; - put_u24_le(len3, static_cast(p.payload.size())); + // Build record: [type u8][len u24][payload][crc32 u32] + std::uint8_t len3[3]; + put_u24_le(len3, static_cast(p.payload.size())); - std::uint8_t head[4]; - head[0] = static_cast(p.type); - head[1] = len3[0]; - head[2] = len3[1]; - head[3] = len3[2]; + std::uint8_t head[4]; + head[0] = static_cast(p.type); + head[1] = len3[0]; + head[2] = len3[1]; + head[3] = len3[2]; - std::uint32_t c = 0; - c = crc32(head, sizeof(head), c); - if (!p.payload.empty()) - c = crc32(p.payload.data(), p.payload.size(), c); - std::uint8_t crcbytes[4]; - crcbytes[0] = static_cast(c & 0xFFu); - crcbytes[1] = static_cast((c >> 8) & 0xFFu); - crcbytes[2] = static_cast((c >> 16) & 0xFFu); - crcbytes[3] = static_cast((c >> 24) & 0xFFu); + std::uint32_t c = 0; + c = crc32(head, sizeof(head), c); + if (!p.payload.empty()) + c = crc32(p.payload.data(), p.payload.size(), c); + std::uint8_t crcbytes[4]; + crcbytes[0] = static_cast(c & 0xFFu); + crcbytes[1] = static_cast((c >> 8) & 0xFFu); + crcbytes[2] = static_cast((c >> 16) & 0xFFu); + crcbytes[3] = static_cast((c >> 24) & 0xFFu); - std::vector rec; - rec.reserve(sizeof(head) + p.payload.size() + sizeof(crcbytes)); - rec.insert(rec.end(), head, head + sizeof(head)); - if (!p.payload.empty()) - rec.insert(rec.end(), p.payload.begin(), p.payload.end()); - rec.insert(rec.end(), crcbytes, crcbytes + sizeof(crcbytes)); + std::vector rec; + rec.reserve(sizeof(head) + p.payload.size() + sizeof(crcbytes)); + rec.insert(rec.end(), head, head + sizeof(head)); + if (!p.payload.empty()) + rec.insert(rec.end(), p.payload.begin(), p.payload.end()); + rec.insert(rec.end(), crcbytes, crcbytes + sizeof(crcbytes)); - // Write (handle partial writes and check results) - bool ok = write_full(ctxp->fd, rec.data(), rec.size()); - if (ok) { + // Write (handle partial writes and check results) + bool ok = write_full(ctxp->fd, rec.data(), rec.size()); + if (!ok) { + int err = errno; + report_error("Failed to write swap record to '" + path + "': " + std::strerror(err), p.buf); + return; + } ctxp->approx_size_bytes += static_cast(rec.size()); if (p.urgent_flush) { - (void) ::fsync(ctxp->fd); + if (::fsync(ctxp->fd) != 0) { + int err = errno; + report_error("Failed to fsync swap file '" + path + "': " + std::strerror(err), p.buf); + } ctxp->last_fsync_ns = now_ns(); } if (p.type == SwapRecType::CHKPT && compact_bytes > 0 && ctxp->approx_size_bytes >= static_cast(compact_bytes)) { - (void) compact_to_checkpoint(*ctxp, rec); + std::string compact_err; + if (!compact_to_checkpoint(*ctxp, rec, compact_err)) { + report_error(compact_err, p.buf); + } } + } catch (const std::exception &e) { + report_error(std::string("Exception in process_one: ") + e.what(), p.buf); + } catch (...) { + report_error("Unknown exception in process_one", p.buf); } - (void) ok; // best-effort; future work could mark ctx error state } @@ -1184,8 +1253,10 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string & switch (type) { case SwapRecType::INS: { std::size_t off = 0; - if (payload.empty()) { - err = "Swap record missing INS payload"; + // INS payload: encver(1) + row(4) + col(4) + nbytes(4) + data(nbytes) + // Minimum: 1 + 4 + 4 + 4 = 13 bytes + if (payload.size() < 13) { + err = "INS payload too short (need at least 13 bytes)"; return false; } const std::uint8_t encver = payload[off++]; @@ -1196,7 +1267,7 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string & std::uint32_t row = 0, col = 0, nbytes = 0; if (!parse_u32_le(payload, off, row) || !parse_u32_le(payload, off, col) || !parse_u32_le( payload, off, nbytes)) { - err = "Malformed INS payload"; + err = "Malformed INS payload (failed to parse row/col/nbytes)"; return false; } if (off + nbytes > payload.size()) { @@ -1209,8 +1280,10 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string & } case SwapRecType::DEL: { std::size_t off = 0; - if (payload.empty()) { - err = "Swap record missing DEL payload"; + // DEL payload: encver(1) + row(4) + col(4) + dlen(4) + // Minimum: 1 + 4 + 4 + 4 = 13 bytes + if (payload.size() < 13) { + err = "DEL payload too short (need at least 13 bytes)"; return false; } const std::uint8_t encver = payload[off++]; @@ -1221,7 +1294,7 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string & std::uint32_t row = 0, col = 0, dlen = 0; if (!parse_u32_le(payload, off, row) || !parse_u32_le(payload, off, col) || !parse_u32_le( payload, off, dlen)) { - err = "Malformed DEL payload"; + err = "Malformed DEL payload (failed to parse row/col/dlen)"; return false; } buf.delete_text((int) row, (int) col, (std::size_t) dlen); @@ -1229,8 +1302,10 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string & } case SwapRecType::SPLIT: { std::size_t off = 0; - if (payload.empty()) { - err = "Swap record missing SPLIT payload"; + // SPLIT payload: encver(1) + row(4) + col(4) + // Minimum: 1 + 4 + 4 = 9 bytes + if (payload.size() < 9) { + err = "SPLIT payload too short (need at least 9 bytes)"; return false; } const std::uint8_t encver = payload[off++]; @@ -1240,7 +1315,7 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string & } std::uint32_t row = 0, col = 0; if (!parse_u32_le(payload, off, row) || !parse_u32_le(payload, off, col)) { - err = "Malformed SPLIT payload"; + err = "Malformed SPLIT payload (failed to parse row/col)"; return false; } buf.split_line((int) row, (int) col); @@ -1248,8 +1323,10 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string & } case SwapRecType::JOIN: { std::size_t off = 0; - if (payload.empty()) { - err = "Swap record missing JOIN payload"; + // JOIN payload: encver(1) + row(4) + // Minimum: 1 + 4 = 5 bytes + if (payload.size() < 5) { + err = "JOIN payload too short (need at least 5 bytes)"; return false; } const std::uint8_t encver = payload[off++]; @@ -1259,7 +1336,7 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string & } std::uint32_t row = 0; if (!parse_u32_le(payload, off, row)) { - err = "Malformed JOIN payload"; + err = "Malformed JOIN payload (failed to parse row)"; return false; } buf.join_lines((int) row); @@ -1267,8 +1344,10 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string & } case SwapRecType::CHKPT: { std::size_t off = 0; + // CHKPT payload: encver(1) + nbytes(4) + data(nbytes) + // Minimum: 1 + 4 = 5 bytes if (payload.size() < 5) { - err = "Malformed CHKPT payload"; + err = "CHKPT payload too short (need at least 5 bytes)"; return false; } const std::uint8_t encver = payload[off++]; @@ -1278,7 +1357,7 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string & } std::uint32_t nbytes = 0; if (!parse_u32_le(payload, off, nbytes)) { - err = "Malformed CHKPT payload"; + err = "Malformed CHKPT payload (failed to parse nbytes)"; return false; } if (off + nbytes > payload.size()) { @@ -1295,4 +1374,54 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string & } } } + + +void +SwapManager::report_error(const std::string &message, Buffer *buf) +{ + std::lock_guard lg(mtx_); + SwapError err; + err.timestamp_ns = now_ns(); + err.message = message; + if (buf && !buf->Filename().empty()) { + err.buffer_name = buf->Filename(); + } else if (buf) { + err.buffer_name = ""; + } else { + err.buffer_name = ""; + } + errors_.push_back(err); + // Bound the error queue to 100 entries + while (errors_.size() > 100) { + errors_.pop_front(); + } + ++total_error_count_; +} + + +bool +SwapManager::HasErrors() const +{ + std::lock_guard lg(mtx_); + return !errors_.empty(); +} + + +std::string +SwapManager::GetLastError() const +{ + std::lock_guard lg(mtx_); + if (errors_.empty()) + return ""; + const SwapError &e = errors_.back(); + return "[" + e.buffer_name + "] " + e.message; +} + + +std::size_t +SwapManager::GetErrorCount() const +{ + std::lock_guard lg(mtx_); + return total_error_count_; +} } // namespace kte \ No newline at end of file diff --git a/Swap.h b/Swap.h index 8d5a3d9..8b991b3 100644 --- a/Swap.h +++ b/Swap.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -131,6 +132,20 @@ public: // Per-buffer toggle void SetSuspended(Buffer &buf, bool on); + // Error reporting for background thread + struct SwapError { + std::uint64_t timestamp_ns{0}; + std::string message; + std::string buffer_name; // filename or "" + }; + + // Query error state (thread-safe) + bool HasErrors() const; + + std::string GetLastError() const; + + std::size_t GetErrorCount() const; + private: class BufferRecorder final : public SwapRecorder { public: @@ -190,11 +205,12 @@ private: static bool write_header(int fd); - static bool open_ctx(JournalCtx &ctx, const std::string &path); + static bool open_ctx(JournalCtx &ctx, const std::string &path, std::string &err); static void close_ctx(JournalCtx &ctx); - static bool compact_to_checkpoint(JournalCtx &ctx, const std::vector &chkpt_record); + static bool compact_to_checkpoint(JournalCtx &ctx, const std::vector &chkpt_record, + std::string &err); static std::uint32_t crc32(const std::uint8_t *data, std::size_t len, std::uint32_t seed = 0); @@ -210,11 +226,14 @@ private: void process_one(const Pending &p); + // Error reporting helper (called from writer thread) + void report_error(const std::string &message, Buffer *buf = nullptr); + // State SwapConfig cfg_{}; std::unordered_map journals_; std::unordered_map > recorders_; - std::mutex mtx_; + mutable std::mutex mtx_; std::condition_variable cv_; std::vector queue_; std::uint64_t next_seq_{0}; @@ -222,5 +241,9 @@ private: std::uint64_t inflight_{0}; std::atomic running_{false}; std::thread worker_; + + // Error tracking (protected by mtx_) + std::deque errors_; // bounded to max 100 entries + std::size_t total_error_count_{0}; }; -} // namespace kte +} // namespace kte \ No newline at end of file diff --git a/main.cc b/main.cc index 81c4138..202ae04 100644 --- a/main.cc +++ b/main.cc @@ -181,124 +181,139 @@ main(int argc, char *argv[]) return RunStressHighlighter(stress_seconds); } - // Determine frontend + // Top-level exception handler to prevent data loss and ensure cleanup + try { + // Determine frontend #if !defined(KTE_BUILD_GUI) - if (req_gui) { - std::cerr << "kte: GUI not built. Reconfigure with -DBUILD_GUI=ON and required deps installed." << - std::endl; - return 2; - } + if (req_gui) { + std::cerr << "kte: GUI not built. Reconfigure with -DBUILD_GUI=ON and required deps installed." + << + std::endl; + return 2; + } #else - bool use_gui = false; - if (req_gui) { - use_gui = true; - } else if (req_term) { - use_gui = false; - } else { + bool use_gui = false; + if (req_gui) { + use_gui = true; + } else if (req_term) { + use_gui = false; + } else { + // Default depends on build target: kge defaults to GUI, kte to terminal #if defined(KTE_DEFAULT_GUI) use_gui = true; #else use_gui = false; #endif - } -#endif - - // Open files passed on the CLI; support +N to jump to line N in the next file. - // If no files are provided, create an empty buffer. - if (optind < argc) { - // Seed a scratch buffer so the UI has something to show while deferred opens - // (and potential swap recovery prompts) are processed. - editor.AddBuffer(Buffer()); - std::size_t pending_line = 0; // 0 = no pending line - for (int i = optind; i < argc; ++i) { - const char *arg = argv[i]; - if (arg && arg[0] == '+') { - // Parse + - const char *p = arg + 1; - if (*p != '\0') { - bool all_digits = true; - for (const char *q = p; *q; ++q) { - if (!std::isdigit(static_cast(*q))) { - all_digits = false; - break; - } - } - if (all_digits) { - // Clamp to >=1 later; 0 disables. - try { - unsigned long v = std::stoul(p); - if (v > std::numeric_limits::max()) { - std::cerr << - "kte: Warning: Line number too large, ignoring\n"; - pending_line = 0; - } else { - pending_line = static_cast(v); - } - } catch (...) { - // Ignore malformed huge numbers - pending_line = 0; - } - continue; // look for the next file arg - } - } - // Fall through: not a +number, treat as filename starting with '+' - } - - const std::string path = arg; - editor.RequestOpenFile(path, pending_line); - pending_line = 0; // consumed (if set) } - // If we ended with a pending +N but no subsequent file, ignore it. - } else { - // Create a single empty buffer - editor.AddBuffer(Buffer()); - editor.SetStatus("new: empty buffer"); - } - - // Install built-in commands - InstallDefaultCommands(); - - // Select frontend - std::unique_ptr fe; -#if defined(KTE_BUILD_GUI) - if (use_gui) { - fe = std::make_unique(); - } else #endif - { - fe = std::make_unique(); - } + + // Open files passed on the CLI; support +N to jump to line N in the next file. + // If no files are provided, create an empty buffer. + if (optind < argc) { + // Seed a scratch buffer so the UI has something to show while deferred opens + // (and potential swap recovery prompts) are processed. + editor.AddBuffer(Buffer()); + std::size_t pending_line = 0; // 0 = no pending line + for (int i = optind; i < argc; ++i) { + const char *arg = argv[i]; + if (arg && arg[0] == '+') { + // Parse + + const char *p = arg + 1; + if (*p != '\0') { + bool all_digits = true; + for (const char *q = p; *q; ++q) { + if (!std::isdigit(static_cast(*q))) { + all_digits = false; + break; + } + } + if (all_digits) { + // Clamp to >=1 later; 0 disables. + try { + unsigned long v = std::stoul(p); + if (v > std::numeric_limits::max()) { + std::cerr << + "kte: Warning: Line number too large, ignoring\n"; + pending_line = 0; + } else { + pending_line = static_cast(v); + } + } catch (...) { + // Ignore malformed huge numbers + pending_line = 0; + } + continue; // look for the next file arg + } + } + // Fall through: not a +number, treat as filename starting with '+' + } + + const std::string path = arg; + editor.RequestOpenFile(path, pending_line); + pending_line = 0; // consumed (if set) + } + // If we ended with a pending +N but no subsequent file, ignore it. + } else { + // Create a single empty buffer + editor.AddBuffer(Buffer()); + editor.SetStatus("new: empty buffer"); + } + + // Install built-in commands + InstallDefaultCommands(); + + // Select frontend + std::unique_ptr fe; +#if defined(KTE_BUILD_GUI) + if (use_gui) { + fe = std::make_unique(); + } else +#endif + { + fe = std::make_unique(); + } #if defined(KTE_BUILD_GUI) && defined(__APPLE__) - if (use_gui) { - /* likely using the .app, so need to cd */ - const char *home = getenv("HOME"); - if (!home) { - std::cerr << "kge.app: HOME environment variable not set" << std::endl; - return 1; + if (use_gui) { + /* likely using the .app, so need to cd */ + const char *home = getenv("HOME"); + if (!home) { + std::cerr << "kge.app: HOME environment variable not set" << std::endl; + return 1; + } + if (chdir(home) != 0) { + std::cerr << "kge.app: failed to chdir to " << home << ": " + << std::strerror(errno) << std::endl; + return 1; + } } - if (chdir(home) != 0) { - std::cerr << "kge.app: failed to chdir to " << home << ": " - << std::strerror(errno) << std::endl; - return 1; - } - } #endif - if (!fe->Init(argc, argv, editor)) { - std::cerr << "kte: failed to initialize frontend" << std::endl; + if (!fe->Init(argc, argv, editor)) { + std::cerr << "kte: failed to initialize frontend" << std::endl; + return 1; + } + + Execute(editor, CommandId::CenterOnCursor); + + bool running = true; + while (running) { + fe->Step(editor, running); + } + + fe->Shutdown(); + + return 0; + } catch (const std::exception &e) { + std::cerr << "\n*** FATAL ERROR ***\n" + << "kte encountered an unhandled exception: " << e.what() << "\n" + << "The editor will now exit. Any unsaved changes may be recovered from swap files.\n"; + return 1; + } catch (...) { + std::cerr << "\n*** FATAL ERROR ***\n" + << "kte encountered an unknown exception.\n" + << "The editor will now exit. Any unsaved changes may be recovered from swap files.\n"; return 1; } - - Execute(editor, CommandId::CenterOnCursor); - - bool running = true; - while (running) { - fe->Step(editor, running); - } - - fe->Shutdown(); - - return 0; -} +} \ No newline at end of file diff --git a/tests/test_swap_edge_cases.cc b/tests/test_swap_edge_cases.cc new file mode 100644 index 0000000..1b61968 --- /dev/null +++ b/tests/test_swap_edge_cases.cc @@ -0,0 +1,813 @@ +#include "Test.h" + +#include "Buffer.h" +#include "Swap.h" + +#include +#include +#include +#include +#include + + +// CRC32 helper (same algorithm as SwapManager::crc32) +static 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; +} + + +// Build a valid 64-byte swap file header +static std::string +build_swap_header() +{ + std::uint8_t hdr[64]; + std::memset(hdr, 0, sizeof(hdr)); + // Magic + const std::uint8_t magic[8] = {'K', 'T', 'E', '_', 'S', 'W', 'P', '\0'}; + std::memcpy(hdr, magic, 8); + // Version = 1 (little-endian) + hdr[8] = 1; + hdr[9] = 0; + hdr[10] = 0; + hdr[11] = 0; + // Flags = 0 + // Created time (just use 0 for tests) + return std::string(reinterpret_cast(hdr), sizeof(hdr)); +} + + +// Build a swap record: [type u8][len u24][payload][crc32 u32] +static std::string +build_swap_record(std::uint8_t type, const std::vector &payload) +{ + std::vector record; + + // Record header: type(1) + length(3) + record.push_back(type); + std::uint32_t len = static_cast(payload.size()); + record.push_back(static_cast(len & 0xFFu)); + record.push_back(static_cast((len >> 8) & 0xFFu)); + record.push_back(static_cast((len >> 16) & 0xFFu)); + + // Payload + record.insert(record.end(), payload.begin(), payload.end()); + + // CRC32 (compute over header + payload) + std::uint32_t crc = crc32(record.data(), record.size()); + record.push_back(static_cast(crc & 0xFFu)); + record.push_back(static_cast((crc >> 8) & 0xFFu)); + record.push_back(static_cast((crc >> 16) & 0xFFu)); + record.push_back(static_cast((crc >> 24) & 0xFFu)); + + return std::string(reinterpret_cast(record.data()), record.size()); +} + + +// Build complete swap file with header and records +static std::string +build_swap_file(const std::vector &records) +{ + std::string file = build_swap_header(); + for (const auto &rec: records) { + file += rec; + } + return file; +} + + +// Write bytes to file +static void +write_file_bytes(const std::string &path, const std::string &bytes) +{ + std::ofstream out(path, std::ios::binary | std::ios::trunc); + out.write(bytes.data(), static_cast(bytes.size())); +} + + +// Helper to encode u32 little-endian +static void +put_u32_le(std::vector &out, std::uint32_t v) +{ + out.push_back(static_cast(v & 0xFFu)); + out.push_back(static_cast((v >> 8) & 0xFFu)); + out.push_back(static_cast((v >> 16) & 0xFFu)); + out.push_back(static_cast((v >> 24) & 0xFFu)); +} + + +//============================================================================= +// 1. MINIMUM VALID PAYLOAD SIZE TESTS +//============================================================================= + +TEST (SwapEdge_INS_MinimumValidPayload) +{ + const std::string path = "./.kte_ut_edge_ins_min.txt"; + const std::string swap_path = "./.kte_ut_edge_ins_min.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "hello\n"); + + // INS record: encver(1) + row(4) + col(4) + nbytes(4) = 13 bytes minimum + // nbytes=0 means zero-length insertion + std::vector payload; + payload.push_back(1); // encver + put_u32_le(payload, 0); // row + put_u32_le(payload, 0); // col + put_u32_le(payload, 0); // nbytes=0 + + std::string rec = build_swap_record(static_cast(kte::SwapRecType::INS), payload); + std::string file = build_swap_file({rec}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_TRUE(kte::SwapManager::ReplayFile(b, swap_path, err)); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} + + +TEST (SwapEdge_DEL_MinimumValidPayload) +{ + const std::string path = "./.kte_ut_edge_del_min.txt"; + const std::string swap_path = "./.kte_ut_edge_del_min.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "hello\n"); + + // DEL record: encver(1) + row(4) + col(4) + dlen(4) = 13 bytes minimum + std::vector payload; + payload.push_back(1); // encver + put_u32_le(payload, 0); // row + put_u32_le(payload, 0); // col + put_u32_le(payload, 0); // dlen=0 + + std::string rec = build_swap_record(static_cast(kte::SwapRecType::DEL), payload); + std::string file = build_swap_file({rec}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_TRUE(kte::SwapManager::ReplayFile(b, swap_path, err)); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} + + +TEST (SwapEdge_SPLIT_MinimumValidPayload) +{ + const std::string path = "./.kte_ut_edge_split_min.txt"; + const std::string swap_path = "./.kte_ut_edge_split_min.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "hello\n"); + + // SPLIT record: encver(1) + row(4) + col(4) = 9 bytes minimum + std::vector payload; + payload.push_back(1); // encver + put_u32_le(payload, 0); // row + put_u32_le(payload, 0); // col + + std::string rec = build_swap_record(static_cast(kte::SwapRecType::SPLIT), payload); + std::string file = build_swap_file({rec}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_TRUE(kte::SwapManager::ReplayFile(b, swap_path, err)); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} + + +TEST (SwapEdge_JOIN_MinimumValidPayload) +{ + const std::string path = "./.kte_ut_edge_join_min.txt"; + const std::string swap_path = "./.kte_ut_edge_join_min.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "hello\nworld\n"); + + // JOIN record: encver(1) + row(4) = 5 bytes minimum + std::vector payload; + payload.push_back(1); // encver + put_u32_le(payload, 0); // row + + std::string rec = build_swap_record(static_cast(kte::SwapRecType::JOIN), payload); + std::string file = build_swap_file({rec}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_TRUE(kte::SwapManager::ReplayFile(b, swap_path, err)); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} + + +TEST (SwapEdge_CHKPT_MinimumValidPayload) +{ + const std::string path = "./.kte_ut_edge_chkpt_min.txt"; + const std::string swap_path = "./.kte_ut_edge_chkpt_min.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "hello\n"); + + // CHKPT record: encver(1) + nbytes(4) = 5 bytes minimum + std::vector payload; + payload.push_back(1); // encver + put_u32_le(payload, 0); // nbytes=0 + + std::string rec = build_swap_record(static_cast(kte::SwapRecType::CHKPT), payload); + std::string file = build_swap_file({rec}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_TRUE(kte::SwapManager::ReplayFile(b, swap_path, err)); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} + + +//============================================================================= +// 2. TRUNCATED PAYLOAD TESTS (BELOW MINIMUM) +//============================================================================= + +TEST (SwapEdge_INS_TruncatedPayload_1Byte) +{ + const std::string path = "./.kte_ut_edge_ins_trunc1.txt"; + const std::string swap_path = "./.kte_ut_edge_ins_trunc1.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "hello\n"); + + // INS record with only 1 byte (just encver) + std::vector payload; + payload.push_back(1); // encver only + + std::string rec = build_swap_record(static_cast(kte::SwapRecType::INS), payload); + std::string file = build_swap_file({rec}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false); + ASSERT_TRUE(err.find("INS payload too short") != std::string::npos); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} + + +TEST (SwapEdge_INS_TruncatedPayload_5Bytes) +{ + const std::string path = "./.kte_ut_edge_ins_trunc5.txt"; + const std::string swap_path = "./.kte_ut_edge_ins_trunc5.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "hello\n"); + + // INS record with 5 bytes (encver + row only) + std::vector payload; + payload.push_back(1); // encver + put_u32_le(payload, 0); // row + + std::string rec = build_swap_record(static_cast(kte::SwapRecType::INS), payload); + std::string file = build_swap_file({rec}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false); + ASSERT_TRUE(err.find("INS payload too short") != std::string::npos); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} + + +TEST (SwapEdge_DEL_TruncatedPayload_9Bytes) +{ + const std::string path = "./.kte_ut_edge_del_trunc9.txt"; + const std::string swap_path = "./.kte_ut_edge_del_trunc9.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "hello\n"); + + // DEL record with 9 bytes (encver + row + col, missing dlen) + std::vector payload; + payload.push_back(1); // encver + put_u32_le(payload, 0); // row + put_u32_le(payload, 0); // col + // missing dlen + + std::string rec = build_swap_record(static_cast(kte::SwapRecType::DEL), payload); + std::string file = build_swap_file({rec}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false); + ASSERT_TRUE(err.find("DEL payload too short") != std::string::npos); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} + + +TEST (SwapEdge_SPLIT_TruncatedPayload_5Bytes) +{ + const std::string path = "./.kte_ut_edge_split_trunc5.txt"; + const std::string swap_path = "./.kte_ut_edge_split_trunc5.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "hello\n"); + + // SPLIT record with 5 bytes (encver + row, missing col) + std::vector payload; + payload.push_back(1); // encver + put_u32_le(payload, 0); // row + // missing col + + std::string rec = build_swap_record(static_cast(kte::SwapRecType::SPLIT), payload); + std::string file = build_swap_file({rec}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false); + ASSERT_TRUE(err.find("SPLIT payload too short") != std::string::npos); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} + + +TEST (SwapEdge_JOIN_TruncatedPayload_1Byte) +{ + const std::string path = "./.kte_ut_edge_join_trunc1.txt"; + const std::string swap_path = "./.kte_ut_edge_join_trunc1.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "hello\nworld\n"); + + // JOIN record with 1 byte (just encver) + std::vector payload; + payload.push_back(1); // encver only + + std::string rec = build_swap_record(static_cast(kte::SwapRecType::JOIN), payload); + std::string file = build_swap_file({rec}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false); + ASSERT_TRUE(err.find("JOIN payload too short") != std::string::npos); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} + + +TEST (SwapEdge_CHKPT_TruncatedPayload_3Bytes) +{ + const std::string path = "./.kte_ut_edge_chkpt_trunc3.txt"; + const std::string swap_path = "./.kte_ut_edge_chkpt_trunc3.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "hello\n"); + + // CHKPT record with 3 bytes (encver + partial nbytes) + std::vector payload; + payload.push_back(1); // encver + payload.push_back(0); // partial nbytes (only 2 bytes instead of 4) + payload.push_back(0); + + std::string rec = build_swap_record(static_cast(kte::SwapRecType::CHKPT), payload); + std::string file = build_swap_file({rec}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false); + ASSERT_TRUE(err.find("CHKPT payload too short") != std::string::npos); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} + + +//============================================================================= +// 3. DATA OVERFLOW TESTS +//============================================================================= + +TEST (SwapEdge_INS_TruncatedData_NbytesExceedsPayload) +{ + const std::string path = "./.kte_ut_edge_ins_overflow.txt"; + const std::string swap_path = "./.kte_ut_edge_ins_overflow.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "hello\n"); + + // INS record where nbytes=100 but payload only contains 13 bytes total + std::vector payload; + payload.push_back(1); // encver + put_u32_le(payload, 0); // row + put_u32_le(payload, 0); // col + put_u32_le(payload, 100); // nbytes=100 (but no data follows) + + std::string rec = build_swap_record(static_cast(kte::SwapRecType::INS), payload); + std::string file = build_swap_file({rec}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false); + ASSERT_TRUE(err.find("Truncated INS payload bytes") != std::string::npos); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} + + +TEST (SwapEdge_CHKPT_TruncatedData_NbytesExceedsPayload) +{ + const std::string path = "./.kte_ut_edge_chkpt_overflow.txt"; + const std::string swap_path = "./.kte_ut_edge_chkpt_overflow.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "hello\n"); + + // CHKPT record where nbytes=1000 but payload only contains 5 bytes total + std::vector payload; + payload.push_back(1); // encver + put_u32_le(payload, 1000); // nbytes=1000 (but no data follows) + + std::string rec = build_swap_record(static_cast(kte::SwapRecType::CHKPT), payload); + std::string file = build_swap_file({rec}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false); + ASSERT_TRUE(err.find("Truncated CHKPT payload bytes") != std::string::npos); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} + + +//============================================================================= +// 4. UNSUPPORTED ENCODING VERSION TESTS +//============================================================================= + +TEST (SwapEdge_INS_UnsupportedEncodingVersion) +{ + const std::string path = "./.kte_ut_edge_ins_badenc.txt"; + const std::string swap_path = "./.kte_ut_edge_ins_badenc.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "hello\n"); + + // INS record with encver=2 (unsupported) + std::vector payload; + payload.push_back(2); // encver=2 (unsupported) + put_u32_le(payload, 0); // row + put_u32_le(payload, 0); // col + put_u32_le(payload, 0); // nbytes + + std::string rec = build_swap_record(static_cast(kte::SwapRecType::INS), payload); + std::string file = build_swap_file({rec}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false); + ASSERT_TRUE(err.find("Unsupported swap payload encoding") != std::string::npos); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} + + +TEST (SwapEdge_CHKPT_UnsupportedEncodingVersion) +{ + const std::string path = "./.kte_ut_edge_chkpt_badenc.txt"; + const std::string swap_path = "./.kte_ut_edge_chkpt_badenc.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "hello\n"); + + // CHKPT record with encver=99 (unsupported) + std::vector payload; + payload.push_back(99); // encver=99 (unsupported) + put_u32_le(payload, 0); // nbytes + + std::string rec = build_swap_record(static_cast(kte::SwapRecType::CHKPT), payload); + std::string file = build_swap_file({rec}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false); + ASSERT_TRUE(err.find("Unsupported swap checkpoint encoding") != std::string::npos); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} + + +//============================================================================= +// 5. BOUNDARY CONDITION TESTS +//============================================================================= + +TEST (SwapEdge_INS_ExactlyEnoughBytes) +{ + const std::string path = "./.kte_ut_edge_ins_exact.txt"; + const std::string swap_path = "./.kte_ut_edge_ins_exact.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "hello\n"); + + // INS record with nbytes=10 and exactly 23 bytes total (13 header + 10 data) + std::vector payload; + payload.push_back(1); // encver + put_u32_le(payload, 0); // row + put_u32_le(payload, 0); // col + put_u32_le(payload, 10); // nbytes=10 + // Add exactly 10 bytes of data + for (int i = 0; i < 10; i++) { + payload.push_back('X'); + } + + std::string rec = build_swap_record(static_cast(kte::SwapRecType::INS), payload); + std::string file = build_swap_file({rec}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_TRUE(kte::SwapManager::ReplayFile(b, swap_path, err)); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} + + +TEST (SwapEdge_INS_OneByteTooFew) +{ + const std::string path = "./.kte_ut_edge_ins_toofew.txt"; + const std::string swap_path = "./.kte_ut_edge_ins_toofew.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "hello\n"); + + // INS record with nbytes=10 but only 22 bytes total (13 header + 9 data) + std::vector payload; + payload.push_back(1); // encver + put_u32_le(payload, 0); // row + put_u32_le(payload, 0); // col + put_u32_le(payload, 10); // nbytes=10 + // Add only 9 bytes of data (one too few) + for (int i = 0; i < 9; i++) { + payload.push_back('X'); + } + + std::string rec = build_swap_record(static_cast(kte::SwapRecType::INS), payload); + std::string file = build_swap_file({rec}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false); + ASSERT_TRUE(err.find("Truncated INS payload bytes") != std::string::npos); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} + + +//============================================================================= +// 6. MIXED VALID AND INVALID RECORDS +//============================================================================= + +TEST (SwapEdge_MixedRecords_ValidThenInvalid) +{ + const std::string path = "./.kte_ut_edge_mixed1.txt"; + const std::string swap_path = "./.kte_ut_edge_mixed1.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "hello\n"); + + // First record: valid INS + std::vector payload1; + payload1.push_back(1); // encver + put_u32_le(payload1, 0); // row + put_u32_le(payload1, 0); // col + put_u32_le(payload1, 1); // nbytes=1 + payload1.push_back('X'); // data + + std::string rec1 = build_swap_record(static_cast(kte::SwapRecType::INS), payload1); + + // Second record: truncated DEL + std::vector payload2; + payload2.push_back(1); // encver only + + std::string rec2 = build_swap_record(static_cast(kte::SwapRecType::DEL), payload2); + + std::string file = build_swap_file({rec1, rec2}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false); + ASSERT_TRUE(err.find("DEL payload too short") != std::string::npos); + + // Verify first INS was applied before failure + auto view = b.GetLineView(0); + std::string line(view.data(), view.size()); + ASSERT_TRUE(line.find('X') != std::string::npos); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} + + +TEST (SwapEdge_MixedRecords_MultipleValidOneInvalid) +{ + const std::string path = "./.kte_ut_edge_mixed2.txt"; + const std::string swap_path = "./.kte_ut_edge_mixed2.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "ab\n"); + + // First record: valid INS at (0,0) + std::vector payload1; + payload1.push_back(1); + put_u32_le(payload1, 0); + put_u32_le(payload1, 0); + put_u32_le(payload1, 1); + payload1.push_back('X'); + std::string rec1 = build_swap_record(static_cast(kte::SwapRecType::INS), payload1); + + // Second record: valid INS at (0,1) + std::vector payload2; + payload2.push_back(1); + put_u32_le(payload2, 0); + put_u32_le(payload2, 1); + put_u32_le(payload2, 1); + payload2.push_back('Y'); + std::string rec2 = build_swap_record(static_cast(kte::SwapRecType::INS), payload2); + + // Third record: truncated SPLIT + std::vector payload3; + payload3.push_back(1); // encver only + std::string rec3 = build_swap_record(static_cast(kte::SwapRecType::SPLIT), payload3); + + std::string file = build_swap_file({rec1, rec2, rec3}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false); + ASSERT_TRUE(err.find("SPLIT payload too short") != std::string::npos); + + // Verify first two INS were applied + auto view = b.GetLineView(0); + std::string line(view.data(), view.size()); + ASSERT_TRUE(line.find('X') != std::string::npos); + ASSERT_TRUE(line.find('Y') != std::string::npos); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} + + +//============================================================================= +// 7. EMPTY PAYLOAD TEST +//============================================================================= + +TEST (SwapEdge_EmptyPayload_INS) +{ + const std::string path = "./.kte_ut_edge_empty.txt"; + const std::string swap_path = "./.kte_ut_edge_empty.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "hello\n"); + + // INS record with zero-length payload + std::vector payload; // empty + + std::string rec = build_swap_record(static_cast(kte::SwapRecType::INS), payload); + std::string file = build_swap_file({rec}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false); + ASSERT_TRUE(err.find("INS payload too short") != std::string::npos); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} + + +//============================================================================= +// 8. CRC MISMATCH TEST +//============================================================================= + +TEST (SwapEdge_ValidStructure_BadCRC) +{ + const std::string path = "./.kte_ut_edge_badcrc.txt"; + const std::string swap_path = "./.kte_ut_edge_badcrc.swp"; + std::remove(path.c_str()); + std::remove(swap_path.c_str()); + + write_file_bytes(path, "hello\n"); + + // Build a valid INS record + std::vector payload; + payload.push_back(1); + put_u32_le(payload, 0); + put_u32_le(payload, 0); + put_u32_le(payload, 1); + payload.push_back('X'); + + std::string rec = build_swap_record(static_cast(kte::SwapRecType::INS), payload); + + // Corrupt the CRC (last 4 bytes) + rec[rec.size() - 1] ^= 0xFF; + + std::string file = build_swap_file({rec}); + write_file_bytes(swap_path, file); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false); + ASSERT_TRUE(err.find("CRC mismatch") != std::string::npos); + + std::remove(path.c_str()); + std::remove(swap_path.c_str()); +} \ No newline at end of file