From 70dc1b8283d125c5c305492d4b2f23c10ba8df93 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 1 Sep 2026 20:06:17 +0200 Subject: [PATCH] feat: add BackupInfo and RollingCrc --- src/backup.zig | 152 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 src/backup.zig diff --git a/src/backup.zig b/src/backup.zig new file mode 100644 index 0000000..6267d07 --- /dev/null +++ b/src/backup.zig @@ -0,0 +1,152 @@ +const std = @import("std"); +const db_mod = @import("db.zig"); +const encode = @import("encode.zig"); + +const Allocator = std.mem.Allocator; +const Db = db_mod.Db; +const Snapshot = db_mod.Snapshot; +const WriteBatch = db_mod.WriteBatch; + +// Portable logical backup format for cobalt. Dumps a consistent, point-in-time +// view of the live keyspace (as returned by Snapshot.scan - tombstones already +// resolved) into a self-describing file that can be replayed into any Db. +// +// Layout: +// header: magic("CBLB" 4B) | version(u32) | seq(u64) +// entry*: keyLen(u32) | valueLen(u32) | key | value | entryCrc(u32) +// entryCrc = crc32(keyLen le bytes || valueLen le bytes || key || value) +// footer: magic("CBLE" 4B) | entryCount(u64) | fileCrc(u32) +// fileCrc = crc32 of every byte written before the footer magic +// (header + all entries), rolling-accumulated while writing. +// +// Restore validates the whole file (magics, per-entry crc, trailing count, +// whole-file crc) before touching the target Db, so a corrupt or truncated +// backup never applies a partial write. + +const headerMagic = "CBLB"; +const footerMagic = "CBLE"; +const formatVersion: u32 = 1; + +pub const BackupError = error{ + BadMagic, + UnsupportedVersion, + Truncated, + Corrupt, + CountMismatch, +}; + +pub const BackupInfo = struct { + seq: u64, + entryCount: u64, +}; + +const RollingCrc = struct { + hasher: std.hash.Crc32, + + fn init() RollingCrc { + return .{ .hasher = std.hash.Crc32.init() }; + } + + fn feed(self: *RollingCrc, bytes: []const u8) void { + self.hasher.update(bytes); + } + + fn value(self: RollingCrc) u32 { + var copy = self.hasher; + return copy.final(); + } +}; + +fn writeAllCrc(writer: anytype, roll: *RollingCrc, bytes: []const u8) !void { + try writer.writeAll(bytes); + roll.feed(bytes); +} + +fn putU32Crc(writer: anytype, roll: *RollingCrc, v: u32) !void { + var b: [4]u8 = undefined; + std.mem.writeInt(u32, &b, v, encode.endian); + try writeAllCrc(writer, roll, &b); +} + +fn putU64Crc(writer: anytype, roll: *RollingCrc, v: u64) !void { + var b: [8]u8 = undefined; + std.mem.writeInt(u64, &b, v, encode.endian); + try writeAllCrc(writer, roll, &b); +} + +/// Writes a full, consistent backup of `snapshot` to `path` (created/truncated). +/// Returns the number of entries written. +pub fn dumpToFile(allocator: Allocator, snapshot: Snapshot, path: []const u8) !u64 { + const file = try std.fs.cwd().createFile(path, .{ .truncate = true }); + defer file.close(); + + var bufferedWriter = std.io.bufferedWriter(file.writer()); + const writer = bufferedWriter.writer(); + + var roll = RollingCrc.init(); + try writeAllCrc(writer, &roll, headerMagic); + try putU32Crc(writer, &roll, formatVersion); + try putU64Crc(writer, &roll, snapshot.seq); + + var iter = try snapshot.scan(allocator, null, null); + defer iter.deinit(); + + var count: u64 = 0; + var entryScratch = std.ArrayList(u8).init(allocator); + defer entryScratch.deinit(); + + while (try iter.next()) |entry| { + entryScratch.clearRetainingCapacity(); + var lenBuf: [8]u8 = undefined; + std.mem.writeInt(u32, lenBuf[0..4], @intCast(entry.key.len), encode.endian); + std.mem.writeInt(u32, lenBuf[4..8], @intCast(entry.value.len), encode.endian); + try entryScratch.appendSlice(&lenBuf); + try entryScratch.appendSlice(entry.key); + try entryScratch.appendSlice(entry.value); + const entryCrc = std.hash.Crc32.hash(entryScratch.items); + + try writeAllCrc(writer, &roll, entryScratch.items); + try putU32Crc(writer, &roll, entryCrc); + count += 1; + } + + try writer.writeAll(footerMagic); + var countBuf: [8]u8 = undefined; + std.mem.writeInt(u64, &countBuf, count, encode.endian); + try writer.writeAll(&countBuf); + + const fileCrc = roll.value(); + var crcBuf: [4]u8 = undefined; + std.mem.writeInt(u32, &crcBuf, fileCrc, encode.endian); + try writer.writeAll(&crcBuf); + + try bufferedWriter.flush(); + return count; +} + +const ParsedEntry = struct { + key: []const u8, + value: []const u8, +}; + +/// One pass over a fully-loaded backup buffer: validates structure end to end +/// (magics, per-entry crc, declared count, whole-file crc) without allocating +/// per-entry copies. Returns the entries slice (positions into `data`) plus +/// backup metadata, or a BackupError describing the first problem found. +const ParsedBackup = struct { + seq: u64, + entries: []ParsedEntry, +}; + +fn parse(allocator: Allocator, data: []const u8) !ParsedBackup { + if (data.len < headerMagic.len + 4 + 8) return BackupError.Truncated; + if (!std.mem.eql(u8, data[0..4], headerMagic)) return BackupError.BadMagic; + + const version = encode.readU32(data, 4); + if (version != formatVersion) return BackupError.UnsupportedVersion; + const seq = encode.readU64(data, 8); + + var pos: usize = 16; + var entries = std.ArrayList(ParsedEntry).init(allocator); + errdefer entries.deinit(); +