diff --git a/src/row.zig b/src/row.zig index 6717801..a4c6f03 100644 --- a/src/row.zig +++ b/src/row.zig @@ -158,3 +158,160 @@ pub const Row = struct { return out.toOwnedSlice(); } + pub fn decode(allocator: Allocator, data: []const u8) !Row { + var row = Row.init(allocator); + errdefer row.deinit(); + var pos: usize = 0; + const fieldCount = try readU32(data, &pos); + var i: u32 = 0; + while (i < fieldCount) : (i += 1) { + const nameLen = try readU16(data, &pos); + if (pos + nameLen > data.len) return error.Malformed; + const name = data[pos .. pos + nameLen]; + pos += nameLen; + if (pos >= data.len) return error.Malformed; + const tag: ValueTag = std.meta.intToEnum(ValueTag, data[pos]) catch return error.Malformed; + pos += 1; + const value: Value = switch (tag) { + .none => .{ .none = {} }, + .boolean => blk: { + if (pos >= data.len) return error.Malformed; + const b = data[pos]; + pos += 1; + break :blk .{ .boolean = b != 0 }; + }, + .int => .{ .int = @bitCast(try readU64(data, &pos)) }, + .float => .{ .float = @bitCast(try readU64(data, &pos)) }, + .text => blk: { + const len = try readU32(data, &pos); + if (pos + len > data.len) return error.Malformed; + const s = data[pos .. pos + len]; + pos += len; + break :blk .{ .text = s }; + }, + .bytes => blk: { + const len = try readU32(data, &pos); + if (pos + len > data.len) return error.Malformed; + const s = data[pos .. pos + len]; + pos += len; + break :blk .{ .bytes = s }; + }, + }; + try row.set(name, value); + } + if (pos != data.len) return error.Malformed; + return row; + } +}; + +fn putU16(out: *std.ArrayList(u8), v: u16) !void { + var b: [2]u8 = undefined; + std.mem.writeInt(u16, &b, v, .little); + try out.appendSlice(&b); +} + +fn putU32(out: *std.ArrayList(u8), v: u32) !void { + var b: [4]u8 = undefined; + std.mem.writeInt(u32, &b, v, .little); + try out.appendSlice(&b); +} + +fn putU64(out: *std.ArrayList(u8), v: u64) !void { + var b: [8]u8 = undefined; + std.mem.writeInt(u64, &b, v, .little); + try out.appendSlice(&b); +} + +fn readU16(data: []const u8, pos: *usize) !u16 { + if (pos.* + 2 > data.len) return error.Malformed; + const v = std.mem.readInt(u16, data[pos.*..][0..2], .little); + pos.* += 2; + return v; +} + +fn readU32(data: []const u8, pos: *usize) !u32 { + if (pos.* + 4 > data.len) return error.Malformed; + const v = std.mem.readInt(u32, data[pos.*..][0..4], .little); + pos.* += 4; + return v; +} + +fn readU64(data: []const u8, pos: *usize) !u64 { + if (pos.* + 8 > data.len) return error.Malformed; + const v = std.mem.readInt(u64, data[pos.*..][0..8], .little); + pos.* += 8; + return v; +} + +/// Appends an order-preserving byte encoding of `value` to `out`, such that +/// lexicographic byte comparison of two encodings matches `compare()` of the +/// original values. Used to build index keys and primary-key suffixes that +/// must sort correctly inside the LSM tree's byte-ordered keyspace. +/// +/// Text/bytes payloads are escaped (0x00 -> 0x00 0xFF) and terminated with +/// 0x00 0x00 so that concatenating several encoded values back to back stays +/// unambiguous and order-preserving (no payload can be a byte-for-byte prefix +/// of another once escaped and terminated). +pub fn appendOrdered(out: *std.ArrayList(u8), value: Value) !void { + try out.append(@intFromEnum(@as(ValueTag, value))); + switch (value) { + .none => {}, + .boolean => |v| try out.append(if (v) 1 else 0), + .int => |v| { + const flipped: u64 = @as(u64, @bitCast(v)) ^ 0x8000000000000000; + var b: [8]u8 = undefined; + std.mem.writeInt(u64, &b, flipped, .big); + try out.appendSlice(&b); + }, + .float => |v| { + var bits: u64 = @bitCast(v); + if (bits & 0x8000000000000000 != 0) { + bits = ~bits; + } else { + bits |= 0x8000000000000000; + } + var b: [8]u8 = undefined; + std.mem.writeInt(u64, &b, bits, .big); + try out.appendSlice(&b); + }, + .text, .bytes => |s| { + for (s) |byte| { + if (byte == 0x00) { + try out.appendSlice(&.{ 0x00, 0xFF }); + } else { + try out.append(byte); + } + } + try out.appendSlice(&.{ 0x00, 0x00 }); + }, + } +} + +pub fn encodeOrdered(allocator: Allocator, value: Value) ![]u8 { + var out = std.ArrayList(u8).init(allocator); + errdefer out.deinit(); + try appendOrdered(&out, value); + return out.toOwnedSlice(); +} + +/// Encodes a single standalone Value using the same (non order-preserving) +/// tag+payload format as a Row field, minus the field name. Used to store +/// primary keys as index-entry values, where later exact reconstruction +/// matters more than byte ordering. +pub fn encodeValue(allocator: Allocator, value: Value) ![]u8 { + var out = std.ArrayList(u8).init(allocator); + errdefer out.deinit(); + try out.append(@intFromEnum(@as(ValueTag, value))); + switch (value) { + .none => {}, + .boolean => |v| try out.append(if (v) 1 else 0), + .int => |v| try putU64(&out, @bitCast(v)), + .float => |v| try putU64(&out, @bitCast(v)), + .text, .bytes => |s| { + try putU32(&out, @intCast(s.len)); + try out.appendSlice(s); + }, + } + return out.toOwnedSlice(); +} +