feat: add BackupInfo and RollingCrc
This commit is contained in:
1 parent
c3e6d8e04a
commit
70dc1b8283
1 file changed
+152
+152
@@ -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();
|
||||||
|
|
||||||
Reference in new issue
Block a user