Initial commit

This commit is contained in:
eleksir
2026-04-02 23:56:28 +03:00
commit 9e30243533
34 changed files with 8413 additions and 0 deletions

View File

@@ -0,0 +1,252 @@
// @tag protocol
// @tag core
package protocol
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"strconv"
)
const (
CRLF = "\r\n"
MaxBulkLen = 512 * 1024 * 1024 // 512 MB limit for bulk strings - prevents memory exhaustion
MaxArrayLen = 1024 * 1024 // 1M elements max - prevents array parsing attacks
)
// Protocol errors - separated for clear error handling
var (
ErrProtocolError = errors.New("protocol error")
ErrNotEnoughData = errors.New("not enough data")
)
// Value represents any RESP value - using interface for simplicity with type switches.
// This mirrors Redis protocol flexibility while keeping implementation simple.
type Value any
// Error represents an error response from Redis (-ERR...)
type Error struct {
Message string
}
func (e Error) Error() string {
return e.Message
}
// Array represents an array of values (*...) - used for commands and responses.
type Array []Value
// Bulk represents a bulk string ($...) - binary safe, used for most arguments.
type Bulk []byte
func (b Bulk) String() string {
return string(b)
}
// Int represents an integer response (:12345) - used for counts and numeric responses.
type Int int64
// Status represents a simple string response (+OK) - used for simple confirmations.
type Status string
func (s Status) String() string {
return string(s)
}
// Parse reads and parses a single RESP value from the reader.
// This is the main entry point for parsing incoming Redis commands.
// Uses bufio.Reader for efficient I/O with built-in buffering.
func Parse(reader *bufio.Reader) (Value, error) {
// Read first line to determine message type
line, err := readLine(reader)
if err != nil {
return nil, err
}
if len(line) == 0 {
return nil, ErrProtocolError
}
// Route based on first character - this is the RESP type indicator
switch line[0] {
case '+': // Simple string: +OK\r\n
return Status(line[1:]), nil
case '-': // Error: -ERR message\r\n
return Error{Message: string(line[1:])}, nil
case ':': // Integer: :12345\r\n
n, err := strconv.ParseInt(string(line[1:]), 10, 64)
if err != nil {
return nil, ErrProtocolError
}
return Int(n), nil
case '$': // Bulk string: $6\r\nfoobar\r\n or $-1 for null
return parseBulk(reader, line)
case '*': // Array: *2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n
return parseArray(reader, line)
default:
return nil, fmt.Errorf("unknown type: %c", line[0])
}
}
// readLine reads a single line from the reader, handling CRLF.
// Uses ReadLine for efficiency - it handles buffering internally.
func readLine(reader *bufio.Reader) ([]byte, error) {
line, isPrefix, err := reader.ReadLine()
if err != nil {
return nil, err
}
if isPrefix {
return nil, fmt.Errorf("line too long")
}
return line, nil
}
// parseBulk handles bulk string parsing ($<length>\r\n<data>\r\n).
// Bulk strings are used for binary-safe string arguments and responses.
func parseBulk(reader *bufio.Reader, line []byte) (Value, error) {
// Parse the length field after the $ prefix
size, err := strconv.Atoi(string(line[1:]))
if err != nil {
return nil, ErrProtocolError
}
// $-1 means null bulk string - legitimate empty value
if size == -1 {
return nil, nil
}
// Validate size to prevent memory exhaustion attacks
if size < 0 || size > MaxBulkLen {
return nil, fmt.Errorf("bulk string too large: %d bytes", size)
}
// Read exactly size bytes plus CRLF delimiter
// Using ReadFull ensures we get all data or an error
buf := make([]byte, size+len(CRLF))
_, err = io.ReadFull(reader, buf)
if err != nil {
return nil, err
}
// Verify proper CRLF termination - prevents protocol injection
if !bytes.HasSuffix(buf, []byte(CRLF)) {
return nil, ErrProtocolError
}
return Bulk(buf[:size]), nil
}
// parseArray handles array parsing (*<count>\r\n<elements>...).
// Arrays are the fundamental structure for Redis commands.
func parseArray(reader *bufio.Reader, line []byte) (Value, error) {
count, err := strconv.Atoi(string(line[1:]))
if err != nil {
return nil, ErrProtocolError
}
// *-1 means null array - rare but valid
if count == -1 {
return nil, nil
}
// Limit array size to prevent DoS via large arrays
if count < 0 || count > MaxArrayLen {
return nil, fmt.Errorf("array too large: %d elements", count)
}
// Pre-allocate with expected size to reduce reallocations
result := make([]Value, count)
for i := range count {
val, err := Parse(reader)
if err != nil {
return nil, err
}
result[i] = val
}
return Array(result), nil
}
// Response builders - these create RESP-encoded responses to send to clients
// MakeError creates an error response (-ERR message).
func MakeError(msg string) []byte {
return []byte("-" + msg + CRLF)
}
// MakeStatus creates a simple status response (+OK).
func MakeStatus(status string) []byte {
return []byte("+" + status + CRLF)
}
// MakeInt creates an integer response (:12345).
func MakeInt(n int64) []byte {
return []byte(":" + strconv.FormatInt(n, 10) + CRLF)
}
// MakeBulk creates a bulk string response ($<len>\r\n<data>\r\n).
// Handles nil gracefully by returning null bulk string.
func MakeBulk(data []byte) []byte {
if data == nil {
return []byte("$-1" + CRLF)
}
return []byte("$" + strconv.Itoa(len(data)) + CRLF + string(data) + CRLF)
}
// MakeMultiBulk creates a RESP array from Value slices - used for pub/sub messages.
func MakeMultiBulk(values []Value) []byte {
var buf bytes.Buffer
buf.WriteString("*" + strconv.Itoa(len(values)) + CRLF)
for _, v := range values {
switch val := v.(type) {
case nil:
buf.WriteString("$-1\r\n")
case string:
buf.Write(MakeBulk([]byte(val)))
case []byte:
buf.Write(MakeBulk(val))
case int64:
buf.Write(MakeInt(val))
case Int:
buf.Write(MakeInt(int64(val)))
case Error:
buf.Write(MakeError(val.Message))
case Status:
buf.Write(MakeStatus(string(val)))
default:
buf.Write(MakeBulk([]byte(fmt.Sprintf("%v", val))))
}
}
return buf.Bytes()
}
// ToBytes converts a parsed Value back to its RESP wire format.
// This is useful for converting parsed commands back to bytes for relay or logging.
// nolint: cyclop
func ToBytes(v Value) []byte {
if v == nil {
return MakeBulk(nil)
}
switch val := v.(type) {
case string:
return MakeBulk([]byte(val))
case []byte:
return MakeBulk(val)
case Bulk:
return MakeBulk([]byte(val))
case int64:
return MakeInt(val)
case Int:
return MakeInt(int64(val))
case Error:
return MakeError(val.Message)
case Status:
return MakeStatus(string(val))
case Array:
return MakeMultiBulk(val)
default:
return MakeBulk([]byte(fmt.Sprintf("%v", val)))
}
}

View File

@@ -0,0 +1,206 @@
package protocol
import (
"bufio"
"bytes"
"testing"
)
func TestParseSimpleString(t *testing.T) {
tests := []struct {
input string
expected Status
}{
{"+OK\r\n", "OK"},
{"+PONG\r\n", "PONG"},
{"+\r\n", ""},
}
for _, tt := range tests {
reader := bufio.NewReader(bytes.NewReader([]byte(tt.input)))
val, err := Parse(reader)
if err != nil {
t.Errorf("Parse(%q) error: %v", tt.input, err)
continue
}
status, ok := val.(Status)
if !ok {
t.Errorf("Parse(%q) got %T, want Status", tt.input, val)
continue
}
if status != tt.expected {
t.Errorf("Parse(%q) = %q, want %q", tt.input, status, tt.expected)
}
}
}
func TestParseError(t *testing.T) {
input := "-ERR unknown command\r\n"
reader := bufio.NewReader(bytes.NewReader([]byte(input)))
val, err := Parse(reader)
if err != nil {
t.Fatalf("Parse error: %v", err)
}
errVal, ok := val.(Error)
if !ok {
t.Fatalf("got %T, want Error", val)
}
if errVal.Message != "ERR unknown command" {
t.Errorf("got %q, want 'ERR unknown command'", errVal.Message)
}
}
func TestParseInteger(t *testing.T) {
tests := []struct {
input string
expected Int
}{
{":0\r\n", 0},
{":12345\r\n", 12345},
{":-100\r\n", -100},
}
for _, tt := range tests {
reader := bufio.NewReader(bytes.NewReader([]byte(tt.input)))
val, err := Parse(reader)
if err != nil {
t.Errorf("Parse(%q) error: %v", tt.input, err)
continue
}
n, ok := val.(Int)
if !ok {
t.Errorf("Parse(%q) got %T, want Int", tt.input, val)
continue
}
if n != tt.expected {
t.Errorf("Parse(%q) = %d, want %d", tt.input, n, tt.expected)
}
}
}
func TestParseBulkString(t *testing.T) {
tests := []struct {
input string
expected []byte
}{
{"$5\r\nhello\r\n", []byte("hello")},
{"$0\r\n\r\n", []byte("")},
{"$-1\r\n", nil},
}
for _, tt := range tests {
reader := bufio.NewReader(bytes.NewReader([]byte(tt.input)))
val, err := Parse(reader)
if err != nil {
t.Errorf("Parse(%q) error: %v", tt.input, err)
continue
}
if tt.expected == nil {
if val != nil {
t.Errorf("Parse(%q) = %v, want nil", tt.input, val)
}
continue
}
bulk, ok := val.(Bulk)
if !ok {
t.Errorf("Parse(%q) got %T, want Bulk", tt.input, val)
continue
}
if string(bulk) != string(tt.expected) {
t.Errorf("Parse(%q) = %q, want %q", tt.input, string(bulk), string(tt.expected))
}
}
}
func TestParseArray(t *testing.T) {
input := "*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"
reader := bufio.NewReader(bytes.NewReader([]byte(input)))
val, err := Parse(reader)
if err != nil {
t.Fatalf("Parse error: %v", err)
}
arr, ok := val.(Array)
if !ok {
t.Fatalf("got %T, want Array", val)
}
if len(arr) != 2 {
t.Errorf("array length = %d, want 2", len(arr))
}
}
func TestParseEmptyArray(t *testing.T) {
input := "*0\r\n"
reader := bufio.NewReader(bytes.NewReader([]byte(input)))
val, err := Parse(reader)
if err != nil {
t.Fatalf("Parse error: %v", err)
}
arr, ok := val.(Array)
if !ok {
t.Fatalf("got %T, want Array", val)
}
if len(arr) != 0 {
t.Errorf("array length = %d, want 0", len(arr))
}
}
func TestMakeStatus(t *testing.T) {
result := MakeStatus("OK")
expected := "+OK\r\n"
if string(result) != expected {
t.Errorf("MakeStatus() = %q, want %q", string(result), expected)
}
}
func TestMakeInt(t *testing.T) {
result := MakeInt(42)
expected := ":42\r\n"
if string(result) != expected {
t.Errorf("MakeInt() = %q, want %q", string(result), expected)
}
}
func TestMakeBulk(t *testing.T) {
tests := []struct {
input []byte
expected string
}{
{[]byte("hello"), "$5\r\nhello\r\n"},
{[]byte(""), "$0\r\n\r\n"},
{nil, "$-1\r\n"},
}
for _, tt := range tests {
result := MakeBulk(tt.input)
if string(result) != tt.expected {
t.Errorf("MakeBulk(%q) = %q, want %q", tt.input, string(result), tt.expected)
}
}
}
func TestMakeMultiBulk(t *testing.T) {
result := MakeMultiBulk([]Value{Bulk("message"), Bulk("mychannel"), Bulk("hello")})
if len(result) == 0 {
t.Error("MakeMultiBulk returned empty")
}
}
func TestToBytes(t *testing.T) {
tests := []struct {
input Value
expected string
}{
{nil, "$-1\r\n"},
{Status("OK"), "+OK\r\n"},
{Int(42), ":42\r\n"},
{Error{Message: "ERR"}, "-ERR\r\n"},
{Bulk("test"), "$4\r\ntest\r\n"},
}
for _, tt := range tests {
result := ToBytes(tt.input)
if string(result) != tt.expected {
t.Errorf("ToBytes(%v) = %q, want %q", tt.input, string(result), tt.expected)
}
}
}