Initial commit
This commit is contained in:
252
internal/protocol/protocol.go
Normal file
252
internal/protocol/protocol.go
Normal 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)))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user