|
@@ -24,7 +24,9 @@ fn parseInteger(buf: []const u8, start: usize, end: usize) ?usize {
|
|
|
var result: usize = 0;
|
|
var result: usize = 0;
|
|
|
for (buf[start..end]) |c| {
|
|
for (buf[start..end]) |c| {
|
|
|
if (c < '0' or c > '9') return null;
|
|
if (c < '0' or c > '9') return null;
|
|
|
- result = result * 10 + (c - '0');
|
|
|
|
|
|
|
+ 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;
|
|
return result;
|
|
|
}
|
|
}
|
|
@@ -38,7 +40,7 @@ fn parseBulkString(buf: []const u8, pos: *usize) ?[]const u8 {
|
|
|
pos.* = len_end + 2;
|
|
pos.* = len_end + 2;
|
|
|
|
|
|
|
|
const str_start = pos.*;
|
|
const str_start = pos.*;
|
|
|
- const str_end = str_start + len;
|
|
|
|
|
|
|
+ const str_end = std.math.add(usize, str_start, len) catch return null;
|
|
|
if (str_end > buf.len) return null;
|
|
if (str_end > buf.len) return null;
|
|
|
|
|
|
|
|
const result = buf[str_start..str_end];
|
|
const result = buf[str_start..str_end];
|
|
@@ -113,23 +115,51 @@ pub fn parseCommand(buf: []const u8) ?ParseResult {
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-fn formatSimpleString(buf: []u8, str: []const u8) []const u8 {
|
|
|
|
|
- var pos: usize = 0;
|
|
|
|
|
- buf[pos] = '+';
|
|
|
|
|
- pos += 1;
|
|
|
|
|
- @memcpy(buf[pos .. pos + str.len], str);
|
|
|
|
|
- pos += str.len;
|
|
|
|
|
- buf[pos] = '\r';
|
|
|
|
|
- buf[pos + 1] = '\n';
|
|
|
|
|
- return buf[0 .. pos + 2];
|
|
|
|
|
|
|
+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 formatBulkString(buf: []u8, str: []const u8) []const u8 {
|
|
|
|
|
- var pos: usize = 0;
|
|
|
|
|
- buf[pos] = '$';
|
|
|
|
|
- pos += 1;
|
|
|
|
|
|
|
+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;
|
|
|
|
|
|
|
|
- pos += formatInt(buf[pos..], str.len);
|
|
|
|
|
|
|
+ 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] = '\r';
|
|
|
buf[pos + 1] = '\n';
|
|
buf[pos + 1] = '\n';
|
|
|
pos += 2;
|
|
pos += 2;
|
|
@@ -142,31 +172,8 @@ fn formatBulkString(buf: []u8, str: []const u8) []const u8 {
|
|
|
return buf[0 .. pos + 2];
|
|
return buf[0 .. pos + 2];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-fn formatInt(buf: []u8, value: usize) usize {
|
|
|
|
|
- if (value == 0) {
|
|
|
|
|
- buf[0] = '0';
|
|
|
|
|
- return 1;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- var v = value;
|
|
|
|
|
- var len: usize = 0;
|
|
|
|
|
- var temp: [20]u8 = undefined;
|
|
|
|
|
-
|
|
|
|
|
- while (v > 0) {
|
|
|
|
|
- temp[len] = @intCast('0' + (v % 10));
|
|
|
|
|
- v /= 10;
|
|
|
|
|
- len += 1;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- var i: usize = 0;
|
|
|
|
|
- while (i < len) : (i += 1) {
|
|
|
|
|
- buf[i] = temp[len - 1 - i];
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return len;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-fn formatNullBulkString(buf: []u8) []const u8 {
|
|
|
|
|
|
|
+fn formatNullBulkString(buf: []u8) ?[]const u8 {
|
|
|
|
|
+ if (buf.len < 5) return null;
|
|
|
buf[0] = '$';
|
|
buf[0] = '$';
|
|
|
buf[1] = '-';
|
|
buf[1] = '-';
|
|
|
buf[2] = '1';
|
|
buf[2] = '1';
|
|
@@ -175,17 +182,21 @@ fn formatNullBulkString(buf: []u8) []const u8 {
|
|
|
return buf[0..5];
|
|
return buf[0..5];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-fn formatInteger(buf: []u8, value: i64) []const u8 {
|
|
|
|
|
- var pos: usize = 0;
|
|
|
|
|
- buf[pos] = ':';
|
|
|
|
|
- pos += 1;
|
|
|
|
|
-
|
|
|
|
|
|
|
+fn formatInteger(buf: []u8, value: i64) ?[]const u8 {
|
|
|
|
|
+ var pos: usize = 1;
|
|
|
if (value < 0) {
|
|
if (value < 0) {
|
|
|
- buf[pos] = '-';
|
|
|
|
|
- pos += 1;
|
|
|
|
|
- pos += formatInt(buf[pos..], @intCast(-value));
|
|
|
|
|
|
|
+ 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 {
|
|
} else {
|
|
|
- pos += formatInt(buf[pos..], @intCast(value));
|
|
|
|
|
|
|
+ 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] = '\r';
|
|
@@ -193,18 +204,17 @@ fn formatInteger(buf: []u8, value: i64) []const u8 {
|
|
|
return buf[0 .. pos + 2];
|
|
return buf[0 .. pos + 2];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-fn formatError(buf: []u8, msg: []const u8) []const u8 {
|
|
|
|
|
- var pos: usize = 0;
|
|
|
|
|
- buf[pos] = '-';
|
|
|
|
|
- pos += 1;
|
|
|
|
|
- @memcpy(buf[pos .. pos + msg.len], msg);
|
|
|
|
|
- pos += msg.len;
|
|
|
|
|
- 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 {
|
|
|
|
|
|
|
+pub fn executeCommand(cmd: RedisCommand, response_buf: []u8) ?[]const u8 {
|
|
|
switch (cmd.cmd_type) {
|
|
switch (cmd.cmd_type) {
|
|
|
.SET => {
|
|
.SET => {
|
|
|
if (storage.write(cmd.key, cmd.value)) {
|
|
if (storage.write(cmd.key, cmd.value)) {
|
|
@@ -238,7 +248,7 @@ fn buildRedisArray(parts: []const []const u8) []u8 {
|
|
|
|
|
|
|
|
buf[pos] = '*';
|
|
buf[pos] = '*';
|
|
|
pos += 1;
|
|
pos += 1;
|
|
|
- pos += formatInt(buf[pos..], parts.len);
|
|
|
|
|
|
|
+ pos += formatInt(buf[pos..], parts.len).?;
|
|
|
buf[pos] = '\r';
|
|
buf[pos] = '\r';
|
|
|
buf[pos + 1] = '\n';
|
|
buf[pos + 1] = '\n';
|
|
|
pos += 2;
|
|
pos += 2;
|
|
@@ -246,7 +256,7 @@ fn buildRedisArray(parts: []const []const u8) []u8 {
|
|
|
for (parts) |part| {
|
|
for (parts) |part| {
|
|
|
buf[pos] = '$';
|
|
buf[pos] = '$';
|
|
|
pos += 1;
|
|
pos += 1;
|
|
|
- pos += formatInt(buf[pos..], part.len);
|
|
|
|
|
|
|
+ pos += formatInt(buf[pos..], part.len).?;
|
|
|
buf[pos] = '\r';
|
|
buf[pos] = '\r';
|
|
|
buf[pos + 1] = '\n';
|
|
buf[pos + 1] = '\n';
|
|
|
pos += 2;
|
|
pos += 2;
|
|
@@ -310,63 +320,149 @@ test "parseCommand bytes_consumed" {
|
|
|
try std.testing.expectEqual(input.len, result.bytes_consumed);
|
|
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" {
|
|
test "formatInt zero" {
|
|
|
var buf: [20]u8 = undefined;
|
|
var buf: [20]u8 = undefined;
|
|
|
- const len = formatInt(&buf, 0);
|
|
|
|
|
|
|
+ const len = formatInt(&buf, 0) orelse return error.TestUnexpectedResult;
|
|
|
try std.testing.expectEqualStrings("0", buf[0..len]);
|
|
try std.testing.expectEqualStrings("0", buf[0..len]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
test "formatInt positive" {
|
|
test "formatInt positive" {
|
|
|
var buf: [20]u8 = undefined;
|
|
var buf: [20]u8 = undefined;
|
|
|
- const len = formatInt(&buf, 12345);
|
|
|
|
|
|
|
+ const len = formatInt(&buf, 12345) orelse return error.TestUnexpectedResult;
|
|
|
try std.testing.expectEqualStrings("12345", buf[0..len]);
|
|
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" {
|
|
test "formatSimpleString" {
|
|
|
var buf: [64]u8 = undefined;
|
|
var buf: [64]u8 = undefined;
|
|
|
- const result = formatSimpleString(&buf, "OK");
|
|
|
|
|
|
|
+ const result = formatSimpleString(&buf, "OK") orelse return error.TestUnexpectedResult;
|
|
|
try std.testing.expectEqualStrings("+OK\r\n", result);
|
|
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" {
|
|
test "formatBulkString" {
|
|
|
var buf: [64]u8 = undefined;
|
|
var buf: [64]u8 = undefined;
|
|
|
- const result = formatBulkString(&buf, "hello");
|
|
|
|
|
|
|
+ const result = formatBulkString(&buf, "hello") orelse return error.TestUnexpectedResult;
|
|
|
try std.testing.expectEqualStrings("$5\r\nhello\r\n", result);
|
|
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" {
|
|
test "formatNullBulkString" {
|
|
|
var buf: [64]u8 = undefined;
|
|
var buf: [64]u8 = undefined;
|
|
|
- const result = formatNullBulkString(&buf);
|
|
|
|
|
|
|
+ const result = formatNullBulkString(&buf) orelse return error.TestUnexpectedResult;
|
|
|
try std.testing.expectEqualStrings("$-1\r\n", result);
|
|
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" {
|
|
test "formatError" {
|
|
|
var buf: [64]u8 = undefined;
|
|
var buf: [64]u8 = undefined;
|
|
|
- const result = formatError(&buf, "ERR bad");
|
|
|
|
|
|
|
+ const result = formatError(&buf, "ERR bad") orelse return error.TestUnexpectedResult;
|
|
|
try std.testing.expectEqualStrings("-ERR bad\r\n", result);
|
|
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" {
|
|
test "formatInteger positive" {
|
|
|
var buf: [64]u8 = undefined;
|
|
var buf: [64]u8 = undefined;
|
|
|
- const result = formatInteger(&buf, 42);
|
|
|
|
|
|
|
+ const result = formatInteger(&buf, 42) orelse return error.TestUnexpectedResult;
|
|
|
try std.testing.expectEqualStrings(":42\r\n", result);
|
|
try std.testing.expectEqualStrings(":42\r\n", result);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
test "formatInteger zero" {
|
|
test "formatInteger zero" {
|
|
|
var buf: [64]u8 = undefined;
|
|
var buf: [64]u8 = undefined;
|
|
|
- const result = formatInteger(&buf, 0);
|
|
|
|
|
|
|
+ const result = formatInteger(&buf, 0) orelse return error.TestUnexpectedResult;
|
|
|
try std.testing.expectEqualStrings(":0\r\n", result);
|
|
try std.testing.expectEqualStrings(":0\r\n", result);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
test "formatInteger negative" {
|
|
test "formatInteger negative" {
|
|
|
var buf: [64]u8 = undefined;
|
|
var buf: [64]u8 = undefined;
|
|
|
- const result = formatInteger(&buf, -7);
|
|
|
|
|
|
|
+ const result = formatInteger(&buf, -7) orelse return error.TestUnexpectedResult;
|
|
|
try std.testing.expectEqualStrings(":-7\r\n", result);
|
|
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" {
|
|
test "executeCommand UNKNOWN" {
|
|
|
var buf: [256]u8 = undefined;
|
|
var buf: [256]u8 = undefined;
|
|
|
const cmd = RedisCommand{ .cmd_type = .UNKNOWN, .key = "", .value = "" };
|
|
const cmd = RedisCommand{ .cmd_type = .UNKNOWN, .key = "", .value = "" };
|
|
|
- const result = executeCommand(cmd, &buf);
|
|
|
|
|
|
|
+ const result = executeCommand(cmd, &buf) orelse return error.TestUnexpectedResult;
|
|
|
try std.testing.expectEqualStrings("-ERR unknown command\r\n", result);
|
|
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, "$"));
|
|
|
|
|
+}
|