feat: add putLenPrefixed and encodeSet to protocol

This commit is contained in:
dualfroz committed 2026-09-07 18:42:33 +02:00
1 parent 06617d9292
commit 4c072af95d
1 file changed
+157
+157
View File
@@ -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 ---