From eb441341cb68ea3e9e230dc5f6ad510f0f7a2cc8 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 7 Sep 2026 19:08:01 +0200 Subject: [PATCH] feat: add fromByte and kind to protocol --- src/protocol.zig | 160 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/src/protocol.zig b/src/protocol.zig index 00a67c5..b05ec99 100644 --- a/src/protocol.zig +++ b/src/protocol.zig @@ -489,3 +489,163 @@ pub const CollAction = enum(u8) { zset_rangebyscore = 28, zset_rank = 29, + pub fn fromByte(b: u8) !CollAction { + return std.meta.intToEnum(CollAction, b) catch error.Malformed; + } + + /// Which `collections.Kind` namespace this action's lazy TTL check + /// (`collttl.expireIfDue`, called on `name` before every dispatch in + /// `handleCollOp`) and `requiredRole` gating both need - grouped by the + /// action's numeric range rather than a second explicit table, since + /// the enum above is itself laid out kind-by-kind. + pub fn kind(self: CollAction) collmod.Kind { + return switch (self) { + .counter_get, .counter_set, .counter_incrby => .counter, + .hash_set, .hash_get, .hash_del, .hash_len, .hash_getall => .hash, + .set_add, .set_rem, .set_ismember, .set_card, .set_members, .set_inter, .set_union, .set_diff => .set, + .list_lpush, .list_rpush, .list_lpop, .list_rpop, .list_len, .list_range => .list, + .zset_add, .zset_score, .zset_incrby, .zset_rem, .zset_card, .zset_rangebyrank, .zset_rangebyscore, .zset_rank => .zset, + }; + } + + /// True for actions that only read - see `server.zig`'s `requiredRole`, + /// which needs this to decide `readonly` vs `readwrite` from the action + /// byte alone, the same action-byte peek `ttl_op`/`coll_ttl_op` already + /// use for their own bundled actions. + pub fn isReadOnly(self: CollAction) bool { + return switch (self) { + .counter_get, .hash_get, .hash_len, .hash_getall, .set_ismember, .set_card, .set_members, .set_inter, .set_union, .set_diff, .list_len, .list_range, .zset_score, .zset_card, .zset_rangebyrank, .zset_rangebyscore, .zset_rank => true, + else => false, + }; + } +}; + +pub fn encodeCollOp(allocator: Allocator, action: CollAction, name: []const u8, args: []const []const u8) ![]u8 { + var buf = std.ArrayList(u8).init(allocator); + errdefer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + try w.byte(@intFromEnum(action)); + try putLenPrefixed(w, name); + try w.putU32(@intCast(args.len)); + for (args) |a| try putLenPrefixed(w, a); + return buf.toOwnedSlice(); +} + +pub const CollOpArgs = struct { + action: CollAction, + name: []const u8, + args: [][]const u8, +}; + +/// `name` and every entry of `args` are borrowed slices into `payload`, +/// valid only as long as the frame that carried them is alive - the same +/// convention `decodeBatch`/`decodeTableSelect` use. Only the `args` +/// pointer array itself is owned by the caller (free with +/// `allocator.free`); never its contents. +pub fn decodeCollOp(allocator: Allocator, payload: []const u8) !CollOpArgs { + var c = Cursor{ .data = payload }; + const action = try CollAction.fromByte(try c.takeByte()); + const name = try c.takeLenPrefixed(); + const count = try c.takeU32(); + var list = try std.ArrayList([]const u8).initCapacity(allocator, count); + errdefer list.deinit(); + var i: u32 = 0; + while (i < count) : (i += 1) { + list.appendAssumeCapacity(try c.takeLenPrefixed()); + } + return .{ .action = action, .name = name, .args = try list.toOwnedSlice() }; +} + +/// One reply shape covers every `coll_op` action, tagged by the first byte - +/// the response-side counterpart of the request-side `CollAction`. `.nil` +/// is the wire form of the CLI's `(nil)` (a hash field/list pop/zset score +/// that does not exist); `.ok` is a bare acknowledgement (counter set, +/// hash set/del as a flag would double up with `.int`, so those still use +/// `.int` with 0/1 - see `encodeCollReply`). +pub const CollReplyTag = enum(u8) { + nil = 0, + ok = 1, + int = 2, + real = 3, + bytes = 4, + stringList = 5, + pairList = 6, + scorePairList = 7, +}; + +pub const CollPair = struct { field: []const u8, value: []const u8 }; +pub const CollScorePair = struct { member: []const u8, score: f64 }; + +pub const CollReply = union(enum) { + nil, + ok, + int: i64, + real: f64, + bytes: []const u8, + stringList: []const []const u8, + pairList: []const CollPair, + scorePairList: []const CollScorePair, +}; + +pub fn encodeCollReply(allocator: Allocator, reply: CollReply) ![]u8 { + var buf = std.ArrayList(u8).init(allocator); + errdefer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + switch (reply) { + .nil => try w.byte(@intFromEnum(CollReplyTag.nil)), + .ok => try w.byte(@intFromEnum(CollReplyTag.ok)), + .int => |v| { + try w.byte(@intFromEnum(CollReplyTag.int)); + try w.putU64(@bitCast(v)); + }, + .real => |v| { + try w.byte(@intFromEnum(CollReplyTag.real)); + try w.putU64(@bitCast(v)); + }, + .bytes => |v| { + try w.byte(@intFromEnum(CollReplyTag.bytes)); + try w.slice(v); + }, + .stringList => |items| { + try w.byte(@intFromEnum(CollReplyTag.stringList)); + try w.putU32(@intCast(items.len)); + for (items) |it| try putLenPrefixed(w, it); + }, + .pairList => |items| { + try w.byte(@intFromEnum(CollReplyTag.pairList)); + try w.putU32(@intCast(items.len)); + for (items) |p| { + try putLenPrefixed(w, p.field); + try putLenPrefixed(w, p.value); + } + }, + .scorePairList => |items| { + try w.byte(@intFromEnum(CollReplyTag.scorePairList)); + try w.putU32(@intCast(items.len)); + for (items) |p| { + try putLenPrefixed(w, p.member); + try w.putU64(@bitCast(p.score)); + } + }, + } + return buf.toOwnedSlice(); +} + +pub const OwnedCollPair = struct { field: []u8, value: []u8 }; +pub const OwnedCollScorePair = struct { member: []u8, score: f64 }; + +/// Decoded, independently owned counterpart of `CollReply` - every byte +/// slice is a fresh allocation the caller must free via `deinit`, unlike +/// `CollOpArgs` above which stays borrowed (that side is consumed +/// immediately inside `dispatch` before the frame is freed; this side +/// crosses back out to a `Client` caller that keeps it around). +pub const OwnedCollReply = union(enum) { + nil, + ok, + int: i64, + real: f64, + bytes: []u8, + stringList: [][]u8, + pairList: []OwnedCollPair, + scorePairList: []OwnedCollScorePair, +