| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468 |
- const std = @import("std");
- const storage = @import("storage.zig");
- const CommandType = enum {
- SET,
- GET,
- DEL,
- UNKNOWN,
- };
- pub const RedisCommand = struct {
- cmd_type: CommandType,
- key: []const u8,
- value: []const u8,
- };
- pub const ParseResult = struct {
- cmd: RedisCommand,
- bytes_consumed: usize,
- };
- fn parseInteger(buf: []const u8, start: usize, end: usize) ?usize {
- if (start >= end) return null;
- var result: usize = 0;
- for (buf[start..end]) |c| {
- if (c < '0' or c > '9') return null;
- const digit: usize = c - '0';
- result = std.math.mul(usize, result, 10) catch return null;
- result = std.math.add(usize, result, digit) catch return null;
- }
- return result;
- }
- fn parseBulkString(buf: []const u8, pos: *usize) ?[]const u8 {
- if (pos.* >= buf.len or buf[pos.*] != '$') return null;
- pos.* += 1;
- const len_end = std.mem.indexOfScalarPos(u8, buf, pos.*, '\r') orelse return null;
- const len = parseInteger(buf, pos.*, len_end) orelse return null;
- pos.* = len_end + 2;
- const str_start = pos.*;
- const str_end = std.math.add(usize, str_start, len) catch return null;
- if (str_end > buf.len) return null;
- const result = buf[str_start..str_end];
- pos.* = str_end + 2;
- return result;
- }
- pub fn parseCommand(buf: []const u8) ?ParseResult {
- if (buf.len == 0) return null;
- var pos: usize = 0;
- if (buf[pos] != '*') return null;
- pos += 1;
- const array_len_end = std.mem.indexOfScalarPos(u8, buf, pos, '\r') orelse return null;
- const array_len = parseInteger(buf, pos, array_len_end) orelse return null;
- pos = array_len_end + 2;
- if (array_len < 1 or array_len > 16) return null;
- var elements: [16][]const u8 = undefined;
- for (0..array_len) |i| {
- elements[i] = parseBulkString(buf, &pos) orelse return null;
- }
- const cmd_str = elements[0];
- var cmd: RedisCommand = undefined;
- if (cmd_str.len == 3) {
- const upper: u32 = (@as(u32, cmd_str[0]) & 0xDF) << 16 | (@as(u32, cmd_str[1]) & 0xDF) << 8 | (@as(u32, cmd_str[2]) & 0xDF);
- if (upper == (@as(u32, 'S') << 16 | @as(u32, 'E') << 8 | @as(u32, 'T'))) {
- if (array_len < 3) return null;
- cmd = RedisCommand{
- .cmd_type = .SET,
- .key = elements[1],
- .value = elements[2],
- };
- } else if (upper == (@as(u32, 'G') << 16 | @as(u32, 'E') << 8 | @as(u32, 'T'))) {
- if (array_len < 2) return null;
- cmd = RedisCommand{
- .cmd_type = .GET,
- .key = elements[1],
- .value = "",
- };
- } else if (upper == (@as(u32, 'D') << 16 | @as(u32, 'E') << 8 | @as(u32, 'L'))) {
- if (array_len < 2) return null;
- cmd = RedisCommand{
- .cmd_type = .DEL,
- .key = elements[1],
- .value = "",
- };
- } else {
- cmd = RedisCommand{
- .cmd_type = .UNKNOWN,
- .key = "",
- .value = "",
- };
- }
- } else {
- cmd = RedisCommand{
- .cmd_type = .UNKNOWN,
- .key = "",
- .value = "",
- };
- }
- return ParseResult{
- .cmd = cmd,
- .bytes_consumed = pos,
- };
- }
- fn intDigits(value: usize) usize {
- if (value == 0) return 1;
- var v = value;
- var d: usize = 0;
- while (v > 0) : (v /= 10) d += 1;
- return d;
- }
- fn formatInt(buf: []u8, value: usize) ?usize {
- if (value == 0) {
- if (buf.len < 1) return null;
- buf[0] = '0';
- return 1;
- }
- const len = intDigits(value);
- if (buf.len < len) return null;
- var v = value;
- var i: usize = len;
- while (i > 0) {
- i -= 1;
- buf[i] = @intCast('0' + (v % 10));
- v /= 10;
- }
- return len;
- }
- fn formatSimpleString(buf: []u8, str: []const u8) ?[]const u8 {
- const needed = 1 + str.len + 2;
- if (buf.len < needed) return null;
- buf[0] = '+';
- @memcpy(buf[1 .. 1 + str.len], str);
- buf[1 + str.len] = '\r';
- buf[2 + str.len] = '\n';
- return buf[0..needed];
- }
- fn formatBulkString(buf: []u8, str: []const u8) ?[]const u8 {
- const needed = 1 + intDigits(str.len) + 2 + str.len + 2;
- if (buf.len < needed) return null;
- buf[0] = '$';
- var pos: usize = 1;
- pos += formatInt(buf[pos..], str.len) orelse return null;
- buf[pos] = '\r';
- buf[pos + 1] = '\n';
- pos += 2;
- @memcpy(buf[pos .. pos + str.len], str);
- pos += str.len;
- buf[pos] = '\r';
- buf[pos + 1] = '\n';
- return buf[0 .. pos + 2];
- }
- fn formatNullBulkString(buf: []u8) ?[]const u8 {
- if (buf.len < 5) return null;
- buf[0] = '$';
- buf[1] = '-';
- buf[2] = '1';
- buf[3] = '\r';
- buf[4] = '\n';
- return buf[0..5];
- }
- fn formatInteger(buf: []u8, value: i64) ?[]const u8 {
- var pos: usize = 1;
- if (value < 0) {
- const needed = 1 + 1 + intDigits(@intCast(-value)) + 2;
- if (buf.len < needed) return null;
- buf[0] = ':';
- buf[1] = '-';
- pos = 2;
- pos += formatInt(buf[pos..], @intCast(-value)) orelse return null;
- } else {
- const needed = 1 + intDigits(@intCast(value)) + 2;
- if (buf.len < needed) return null;
- buf[0] = ':';
- pos = 1;
- pos += formatInt(buf[pos..], @intCast(value)) orelse return null;
- }
- buf[pos] = '\r';
- buf[pos + 1] = '\n';
- return buf[0 .. pos + 2];
- }
- pub fn formatError(buf: []u8, msg: []const u8) ?[]const u8 {
- const needed = 1 + msg.len + 2;
- if (buf.len < needed) return null;
- buf[0] = '-';
- @memcpy(buf[1 .. 1 + msg.len], msg);
- buf[1 + msg.len] = '\r';
- buf[2 + msg.len] = '\n';
- return buf[0..needed];
- }
- pub fn executeCommand(cmd: RedisCommand, response_buf: []u8) ?[]const u8 {
- switch (cmd.cmd_type) {
- .SET => {
- if (storage.write(cmd.key, cmd.value)) {
- return formatSimpleString(response_buf, "OK");
- } else {
- return formatError(response_buf, "ERR write failed");
- }
- },
- .GET => {
- if (storage.read(cmd.key)) |value| {
- return formatBulkString(response_buf, value);
- } else {
- return formatNullBulkString(response_buf);
- }
- },
- .DEL => {
- const deleted = storage.delete(cmd.key);
- return formatInteger(response_buf, if (deleted) 1 else 0);
- },
- .UNKNOWN => {
- return formatError(response_buf, "ERR unknown command");
- },
- }
- }
- // -- Tests --
- fn buildRedisArray(parts: []const []const u8) []u8 {
- var buf: [4096]u8 = undefined;
- var pos: usize = 0;
- buf[pos] = '*';
- pos += 1;
- pos += formatInt(buf[pos..], parts.len).?;
- buf[pos] = '\r';
- buf[pos + 1] = '\n';
- pos += 2;
- for (parts) |part| {
- buf[pos] = '$';
- pos += 1;
- pos += formatInt(buf[pos..], part.len).?;
- buf[pos] = '\r';
- buf[pos + 1] = '\n';
- pos += 2;
- @memcpy(buf[pos .. pos + part.len], part);
- pos += part.len;
- buf[pos] = '\r';
- buf[pos + 1] = '\n';
- pos += 2;
- }
- return buf[0..pos];
- }
- test "parseCommand SET" {
- const input = buildRedisArray(&.{ "SET", "mykey", "myvalue" });
- const result = parseCommand(input) orelse return error.TestUnexpectedResult;
- try std.testing.expectEqual(CommandType.SET, result.cmd.cmd_type);
- try std.testing.expectEqualStrings("mykey", result.cmd.key);
- try std.testing.expectEqualStrings("myvalue", result.cmd.value);
- }
- test "parseCommand GET" {
- const input = buildRedisArray(&.{ "GET", "mykey" });
- const result = parseCommand(input) orelse return error.TestUnexpectedResult;
- try std.testing.expectEqual(CommandType.GET, result.cmd.cmd_type);
- try std.testing.expectEqualStrings("mykey", result.cmd.key);
- }
- test "parseCommand DEL" {
- const input = buildRedisArray(&.{ "DEL", "mykey" });
- const result = parseCommand(input) orelse return error.TestUnexpectedResult;
- try std.testing.expectEqual(CommandType.DEL, result.cmd.cmd_type);
- try std.testing.expectEqualStrings("mykey", result.cmd.key);
- }
- test "parseCommand case insensitive" {
- const input = buildRedisArray(&.{ "set", "k", "v" });
- const result = parseCommand(input) orelse return error.TestUnexpectedResult;
- try std.testing.expectEqual(CommandType.SET, result.cmd.cmd_type);
- }
- test "parseCommand unknown command" {
- const input = buildRedisArray(&.{ "FOO", "bar" });
- const result = parseCommand(input) orelse return error.TestUnexpectedResult;
- try std.testing.expectEqual(CommandType.UNKNOWN, result.cmd.cmd_type);
- }
- test "parseCommand empty input" {
- try std.testing.expectEqual(@as(?ParseResult, null), parseCommand(""));
- }
- test "parseCommand malformed input" {
- try std.testing.expectEqual(@as(?ParseResult, null), parseCommand("garbage"));
- try std.testing.expectEqual(@as(?ParseResult, null), parseCommand("*"));
- try std.testing.expectEqual(@as(?ParseResult, null), parseCommand("*1\r\n"));
- }
- test "parseCommand bytes_consumed" {
- const input = buildRedisArray(&.{ "GET", "key1" });
- const result = parseCommand(input) orelse return error.TestUnexpectedResult;
- try std.testing.expectEqual(input.len, result.bytes_consumed);
- }
- test "parseInteger max usize fits" {
- const s = "18446744073709551615";
- try std.testing.expectEqual(@as(?usize, std.math.maxInt(usize)), parseInteger(s, 0, s.len));
- }
- test "parseInteger overflow returns null" {
- try std.testing.expectEqual(@as(?usize, null), parseInteger("18446744073709551616", 0, 20));
- try std.testing.expectEqual(@as(?usize, null), parseInteger("999999999999999999999999999999", 0, 30));
- }
- test "parseCommand huge bulk length returns null" {
- const input = "*2\r\n$3\r\nGET\r\n$18446744073709551615\r\n";
- try std.testing.expectEqual(@as(?ParseResult, null), parseCommand(input));
- }
- test "formatInt zero" {
- var buf: [20]u8 = undefined;
- const len = formatInt(&buf, 0) orelse return error.TestUnexpectedResult;
- try std.testing.expectEqualStrings("0", buf[0..len]);
- }
- test "formatInt positive" {
- var buf: [20]u8 = undefined;
- const len = formatInt(&buf, 12345) orelse return error.TestUnexpectedResult;
- try std.testing.expectEqualStrings("12345", buf[0..len]);
- }
- test "formatInt respects buffer bounds" {
- var buf: [4]u8 = undefined;
- try std.testing.expectEqual(@as(?usize, null), formatInt(buf[0..3], 1234));
- const len = formatInt(buf[0..4], 1234) orelse return error.TestUnexpectedResult;
- try std.testing.expectEqualStrings("1234", buf[0..len]);
- }
- test "formatSimpleString" {
- var buf: [64]u8 = undefined;
- const result = formatSimpleString(&buf, "OK") orelse return error.TestUnexpectedResult;
- try std.testing.expectEqualStrings("+OK\r\n", result);
- }
- test "formatSimpleString respects buffer bounds" {
- var exact: [5]u8 = undefined;
- const ok = formatSimpleString(exact[0..5], "OK") orelse return error.TestUnexpectedResult;
- try std.testing.expectEqualStrings("+OK\r\n", ok);
- var short: [4]u8 = undefined;
- try std.testing.expectEqual(@as(?[]const u8, null), formatSimpleString(short[0..4], "OK"));
- }
- test "formatBulkString" {
- var buf: [64]u8 = undefined;
- const result = formatBulkString(&buf, "hello") orelse return error.TestUnexpectedResult;
- try std.testing.expectEqualStrings("$5\r\nhello\r\n", result);
- }
- test "formatBulkString respects buffer bounds" {
- const value = "hello world";
- const needed = 1 + intDigits(value.len) + 2 + value.len + 2;
- var exact: [64]u8 = undefined;
- const ok = formatBulkString(exact[0..needed], value) orelse return error.TestUnexpectedResult;
- try std.testing.expectEqualStrings("$11\r\nhello world\r\n", ok);
- var short: [64]u8 = undefined;
- try std.testing.expectEqual(@as(?[]const u8, null), formatBulkString(short[0 .. needed - 1], value));
- }
- test "formatNullBulkString" {
- var buf: [64]u8 = undefined;
- const result = formatNullBulkString(&buf) orelse return error.TestUnexpectedResult;
- try std.testing.expectEqualStrings("$-1\r\n", result);
- }
- test "formatNullBulkString respects buffer bounds" {
- var exact: [5]u8 = undefined;
- const ok = formatNullBulkString(exact[0..5]) orelse return error.TestUnexpectedResult;
- try std.testing.expectEqualStrings("$-1\r\n", ok);
- var short: [4]u8 = undefined;
- try std.testing.expectEqual(@as(?[]const u8, null), formatNullBulkString(short[0..4]));
- }
- test "formatError" {
- var buf: [64]u8 = undefined;
- const result = formatError(&buf, "ERR bad") orelse return error.TestUnexpectedResult;
- try std.testing.expectEqualStrings("-ERR bad\r\n", result);
- }
- test "formatError respects buffer bounds" {
- const msg = "ERR bad";
- const needed = 1 + msg.len + 2;
- var exact: [16]u8 = undefined;
- const ok = formatError(exact[0..needed], msg) orelse return error.TestUnexpectedResult;
- try std.testing.expectEqualStrings("-ERR bad\r\n", ok);
- var short: [16]u8 = undefined;
- try std.testing.expectEqual(@as(?[]const u8, null), formatError(short[0 .. needed - 1], msg));
- }
- test "formatInteger positive" {
- var buf: [64]u8 = undefined;
- const result = formatInteger(&buf, 42) orelse return error.TestUnexpectedResult;
- try std.testing.expectEqualStrings(":42\r\n", result);
- }
- test "formatInteger zero" {
- var buf: [64]u8 = undefined;
- const result = formatInteger(&buf, 0) orelse return error.TestUnexpectedResult;
- try std.testing.expectEqualStrings(":0\r\n", result);
- }
- test "formatInteger negative" {
- var buf: [64]u8 = undefined;
- const result = formatInteger(&buf, -7) orelse return error.TestUnexpectedResult;
- try std.testing.expectEqualStrings(":-7\r\n", result);
- }
- test "formatInteger respects buffer bounds" {
- var exact: [16]u8 = undefined;
- const ok = formatInteger(exact[0..5], 42) orelse return error.TestUnexpectedResult;
- try std.testing.expectEqualStrings(":42\r\n", ok);
- var short: [16]u8 = undefined;
- try std.testing.expectEqual(@as(?[]const u8, null), formatInteger(short[0..4], 42));
- }
- test "executeCommand UNKNOWN" {
- var buf: [256]u8 = undefined;
- const cmd = RedisCommand{ .cmd_type = .UNKNOWN, .key = "", .value = "" };
- const result = executeCommand(cmd, &buf) orelse return error.TestUnexpectedResult;
- try std.testing.expectEqualStrings("-ERR unknown command\r\n", result);
- }
- test "executeCommand GET with insufficient buffer returns null" {
- storage.init();
- _ = storage.restore("overflow_key", "this value is far too long to fit in a tiny buffer");
- const cmd = RedisCommand{ .cmd_type = .GET, .key = "overflow_key", .value = "" };
- var tiny: [16]u8 = undefined;
- try std.testing.expectEqual(@as(?[]const u8, null), executeCommand(cmd, tiny[0..]));
- var enough: [512]u8 = undefined;
- const resp = executeCommand(cmd, enough[0..]) orelse return error.TestUnexpectedResult;
- try std.testing.expect(std.mem.startsWith(u8, resp, "$"));
- }
|