253 lines
6.5 KiB
Go
253 lines
6.5 KiB
Go
// @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)))
|
|
}
|
|
}
|