diff --git a/src/protocol.zig b/src/protocol.zig index 2acf48a..4ef0611 100644 --- a/src/protocol.zig +++ b/src/protocol.zig @@ -156,3 +156,160 @@ pub const Cursor = struct { } }; +fn putLenPrefixed(writer: encode.ByteWriter, v: []const u8) !void { + try writer.putU32(@intCast(v.len)); + try writer.slice(v); +} + +// --- set --- + +pub fn encodeSet(allocator: Allocator, key: []const u8, value: []const u8) ![]u8 { + var buf = std.ArrayList(u8).init(allocator); + errdefer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + try putLenPrefixed(w, key); + try w.slice(value); + return buf.toOwnedSlice(); +} + +pub const SetArgs = struct { key: []const u8, value: []const u8 }; + +pub fn decodeSet(payload: []const u8) !SetArgs { + var c = Cursor{ .data = payload }; + const key = try c.takeLenPrefixed(); + const value = c.data[c.pos..]; + return .{ .key = key, .value = value }; +} + +// --- auth --- + +pub fn encodeAuth(allocator: Allocator, username: []const u8, password: []const u8) ![]u8 { + var buf = std.ArrayList(u8).init(allocator); + errdefer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + try putLenPrefixed(w, username); + try w.slice(password); + return buf.toOwnedSlice(); +} + +pub const AuthArgs = struct { username: []const u8, password: []const u8 }; + +pub fn decodeAuth(payload: []const u8) !AuthArgs { + var c = Cursor{ .data = payload }; + const username = try c.takeLenPrefixed(); + const password = c.data[c.pos..]; + return .{ .username = username, .password = password }; +} + +/// A successful `auth` response's payload: empty when the server has no +/// session store wired up (see server.zig's `enableSessions`), or the raw +/// session token bytes when it does. There is no separate decode function +/// on the client side - `frame.payload` itself IS the token (or empty), +/// no framing needed for a single opaque blob. +pub fn encodeAuthOk(allocator: Allocator, token: ?[]const u8) ![]u8 { + return allocator.dupe(u8, token orelse ""); +} + +// --- auth_token --- + +/// Reconnects a fresh connection to a role without presenting the +/// password again, by presenting a token previously handed out in an +/// `auth` response (see `session.zig`'s `SessionStore.issue`). The +/// payload is nothing but the opaque token bytes - no framing needed for +/// a single blob, exactly like `encodeAuthOk`. +pub fn encodeAuthToken(allocator: Allocator, token: []const u8) ![]u8 { + return allocator.dupe(u8, token); +} + +pub fn decodeAuthToken(payload: []const u8) []const u8 { + return payload; +} + +// --- user_admin --- + +/// Remote counterpart of the CLI's local `user add|passwd|role|del|list`, +/// carried as one opcode so `server.zig`'s `requiredRole` can gate all five +/// actions behind a single `admin`-only check instead of five separate +/// opcodes. `role`/`rounds` are only meaningful for the actions that use +/// them (`create` reads both, `set_role` reads only `role`, `set_password` +/// reads only `rounds`); callers send 0 for the fields an action ignores. +pub const UserAdminAction = enum(u8) { + create = 0, + set_password = 1, + set_role = 2, + delete = 3, + list = 4, + + pub fn fromByte(b: u8) !UserAdminAction { + return std.meta.intToEnum(UserAdminAction, b) catch error.Malformed; + } +}; + +pub fn encodeUserAdmin(allocator: Allocator, action: UserAdminAction, username: []const u8, password: []const u8, role: authmod.Role, rounds: u32) ![]u8 { + var buf = std.ArrayList(u8).init(allocator); + errdefer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + try w.byte(@intFromEnum(action)); + try w.byte(@intFromEnum(role)); + try w.putU32(rounds); + try putLenPrefixed(w, username); + try w.slice(password); + return buf.toOwnedSlice(); +} + +pub const UserAdminArgs = struct { + action: UserAdminAction, + roleByte: u8, + rounds: u32, + username: []const u8, + password: []const u8, +}; + +pub fn decodeUserAdmin(payload: []const u8) !UserAdminArgs { + var c = Cursor{ .data = payload }; + const action = try UserAdminAction.fromByte(try c.takeByte()); + const roleByte = try c.takeByte(); + const rounds = try c.takeU32(); + const username = try c.takeLenPrefixed(); + const password = c.data[c.pos..]; + return .{ .action = action, .roleByte = roleByte, .rounds = rounds, .username = username, .password = password }; +} + +/// Encodes the response of a `list` user_admin request: every username +/// paired with its role byte. Reuses `authmod.UserInfo`/`freeUserList` +/// rather than inventing a parallel type, the same way `encodeStats` reuses +/// `dbmod.Stats` and `encodeRows` reuses `rowmod.Row`. +pub fn encodeUserList(allocator: Allocator, users: []const authmod.UserInfo) ![]u8 { + var buf = std.ArrayList(u8).init(allocator); + errdefer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + try w.putU32(@intCast(users.len)); + for (users) |u| { + try putLenPrefixed(w, u.username); + try w.byte(@intFromEnum(u.role)); + } + return buf.toOwnedSlice(); +} + +/// Decodes a user list response into independently owned entries - free +/// with `authmod.freeUserList`. +pub fn decodeUserList(allocator: Allocator, payload: []const u8) ![]authmod.UserInfo { + var c = Cursor{ .data = payload }; + const count = try c.takeU32(); + var list = try std.ArrayList(authmod.UserInfo).initCapacity(allocator, count); + errdefer { + for (list.items) |u| u.deinit(allocator); + list.deinit(); + } + var i: u32 = 0; + while (i < count) : (i += 1) { + const uname = try c.takeLenPrefixed(); + const role = authmod.Role.fromByte(try c.takeByte()) catch return error.Malformed; + const owned = try allocator.dupe(u8, uname); + list.appendAssumeCapacity(.{ .username = owned, .role = role }); + } + return list.toOwnedSlice(); +} + +// --- ttl_op --- +