Files
pekoe/internal/protocol/protocol.go
eleksir 488ce1e4d1 Bugfixes, add INFO command, graceful shutdown. elimitate dead code
- Remove unsed handleClientKill, handleClientKill
- Fix misleading error message on rate limit
- Eliminated unused channel-to-pattern index that was causing unnecessary work
- Fixed O(n²) duplicate check in PublishToPattern
- Limit total pattern subscriptions to 10,000
- Limit channel subscriptions per client to 10,000
- Added INFO command
- Added graceful shutdown timeout
- Validate that log directory is writable during config load
- Make SELECT properly handle whitespace around DB index like Redis
- Return proper error instead of empty array for unsupported commands on sharded pubsub stubs
- More accurate error messages throughout
- Updated README.md
2026-04-03 10:01:29 +03:00

260 lines
6.7 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)
}
// Build directly to avoid string conversion
b := make([]byte, 0, len(data)+len("$1000000000\r\n\r\n"))
b = append(b, '$')
b = strconv.AppendInt(b, int64(len(data)), 10)
b = append(b, CRLF...)
b = append(b, data...)
b = append(b, CRLF...)
return b
}
// 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)))
}
}