// @tag server // @tag core package server import ( "bufio" "fmt" "net" "github.com/eleksir/pekoe/internal/protocol" ) // Command represents a parsed Redis command with arguments. type Command struct { Args [][]byte } // ConnReader handles reading and parsing RESP commands from a connection. // Uses bufio.Reader for efficient I/O with appropriate buffer size. type ConnReader struct { reader *bufio.Reader maxBytes int } // NewConnReader creates a new connection reader with the specified max message size. func NewConnReader(conn net.Conn, maxBytes int) *ConnReader { return &ConnReader{ reader: bufio.NewReaderSize(conn, 16*1024), maxBytes: maxBytes, } } // ReadCommand reads and parses a complete Redis command from the connection. // Returns a Command ready for processing, or an error if parsing fails. // nolint: cyclop func (r *ConnReader) ReadCommand() (*Command, error) { val, err := protocol.Parse(r.reader) if err != nil { return nil, err } if val == nil { return nil, fmt.Errorf("unexpected null value") } // RESP arrays represent commands - the first element is the command name array, ok := val.(protocol.Array) if !ok { return nil, fmt.Errorf("expected array, got %T", val) } if len(array) == 0 { return nil, fmt.Errorf("empty command array") } // Convert array elements to byte slices for command processing cmd := &Command{ Args: make([][]byte, len(array)), } for i, v := range array { var bytes []byte switch item := v.(type) { case protocol.Bulk: bytes = item case []byte: bytes = item case string: bytes = []byte(item) default: return nil, fmt.Errorf("invalid argument type: %T", v) } // Enforce max message size to prevent memory exhaustion if len(bytes) > r.maxBytes { return nil, fmt.Errorf("argument too large: %d bytes (max: %d)", len(bytes), r.maxBytes) } cmd.Args[i] = bytes } return cmd, nil } // ConnWriter handles writing RESP responses to a connection. // Uses buffered writer for efficiency - reduces syscalls. type ConnWriter struct { conn net.Conn writer *bufio.Writer } // NewConnWriter creates a new connection writer with buffering. func NewConnWriter(conn net.Conn) *ConnWriter { return &ConnWriter{ conn: conn, writer: bufio.NewWriterSize(conn, 16*1024), } } // WriteStatus writes a simple status response (+OK). func (w *ConnWriter) WriteStatus(status string) { _, _ = w.writer.Write(protocol.MakeStatus(status)) } // WriteError writes an error response (-ERR message). func (w *ConnWriter) WriteError(err string) { _, _ = w.writer.Write(protocol.MakeError(err)) } // WriteInt writes an integer response (:12345). func (w *ConnWriter) WriteInt(n int64) { _, _ = w.writer.Write(protocol.MakeInt(n)) } // WriteBulk writes a bulk string response ($\r\n). func (w *ConnWriter) WriteBulk(data []byte) { _, _ = w.writer.Write(protocol.MakeBulk(data)) } // WriteArray writes a RESP array response - used for pub/sub confirmations. func (w *ConnWriter) WriteArray(values []protocol.Value) { _, _ = w.writer.Write(protocol.MakeMultiBulk(values)) } // Flush writes all buffered data to the underlying connection. func (w *ConnWriter) Flush() error { return w.writer.Flush() }