// @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 ($\r\n\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 (*\r\n...). // 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 ($\r\n\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))) } }