diff --git a/internal/protcl/errors.go b/internal/protcl/errors.go index 4ece0eb..8c4fa66 100644 --- a/internal/protcl/errors.go +++ b/internal/protcl/errors.go @@ -101,3 +101,29 @@ func (e *ErrUnknownCommand) Error() string { func (ErrUnknownCommand) Recoverable() bool { return true } + +// ErrProtocolType is for protocol type error +type ErrProtocolType struct { + Type byte +} + +func (e *ErrProtocolType) Recoverable() bool { + return true +} + +func (e *ErrProtocolType) Error() string { + return fmt.Sprintf("unknown protocol type: %s", string(e.Type)) +} + +// ErrUnexpectString is for unexpect string error +type ErrUnexpectString struct { + Str string +} + +func (e *ErrUnexpectString) Recoverable() bool { + return true +} + +func (e *ErrUnexpectString) Error() string { + return fmt.Sprintf("unexpect string: %s", e.Str) +} diff --git a/internal/protcl/resp3.go b/internal/protcl/resp3.go new file mode 100644 index 0000000..9384ca5 --- /dev/null +++ b/internal/protcl/resp3.go @@ -0,0 +1,179 @@ +package protcl + +import ( + "bufio" + "fmt" + "strconv" +) + +// resp3 protocol type +const ( + Resp3SimpleString = '+' // +\n + Resp3BlobString = '$' // $\n\n + Resp3VerbatimString = '=' // =\n\n\n + Resp3SimpleError = '-' // -\n + Resp3BolbError = '!' // !\n\n + Resp3Number = ':' // :\n + Resp3Double = ',' // ,\n + Resp3BigNumber = '(' // (\n + Resp3Null = '_' // _\n + Resp3Boolean = '#' // #t\n or #f\n + Resp3Array = '*' // *\n... numelements other types ... + Resp3Map = '%' // %\n... numelements other types ... + Resp3Set = '~' // ~\n... numelements other types ... +) + +// LF is \n +const LF = '\n' + +// Resp3 the response of resp3 protocol +type Resp3 struct { + Type byte + Str string + Integer int + Boolean bool +} + +func (r *Resp3) String() string { + switch r.Type { + case RepSimpleString, Resp3BlobString: + return fmt.Sprintf("%q", r.Str) + case Resp3SimpleError, Resp3BolbError: + return "(error) " + r.Str + case Resp3Number: + return "(integer) " + strconv.Itoa(r.Integer) + case Resp3Null: + return "(null)" + case Resp3Boolean: + if r.Boolean { + return "(boolean) true" + } + return "(boolean) false" + } + + return "(error) unknown protocol type: " + string(r.Type) +} + +// Resp3Parser is for parser resp3 protocol +type Resp3Parser struct { + reader *bufio.Reader +} + +// NewResp3Parser return a Resp3Parser +func NewResp3Parser(r *bufio.Reader) *Resp3Parser { + return &Resp3Parser{reader: r} +} + +// Parse return Resp3 +func (r *Resp3Parser) Parse() (*Resp3, error) { + b, err := r.reader.ReadByte() + if err != nil { + return nil, err + } + + switch b { + case Resp3SimpleString, Resp3SimpleError: + str, err := r.stringBeforeLF() + if err != nil { + return nil, err + } + return &Resp3{Type: b, Str: str}, nil + case Resp3BlobString, Resp3BolbError: + length, err := r.intBeforeLF() + if err != nil { + return nil, err + } + + bs, err := r.readLengthBytesWithLF(length) + if err != nil { + return nil, err + } + + return &Resp3{Type: b, Str: string(bs)}, nil + case Resp3Number: + integer, err := r.intBeforeLF() + if err != nil { + return nil, err + } + return &Resp3{Type: b, Integer: integer}, nil + case Resp3Null: + if _, err := r.readLengthBytesWithLF(0); err != nil { + return nil, err + } + return &Resp3{Type: b}, nil + case Resp3Boolean: + buf, err := r.readLengthBytesWithLF(1) + if err != nil { + return nil, err + } + + switch buf[0] { + case 't': + return &Resp3{Type: b, Boolean: true}, nil + case 'f': + return &Resp3{Type: b, Boolean: false}, nil + } + return nil, &ErrUnexpectString{Str: "t/f"} + } + + return nil, &ErrProtocolType{Type: b} +} + +func (r *Resp3Parser) stringBeforeLF() (string, error) { + buf, err := r.reader.ReadBytes(LF) + if err != nil { + return "", err + } + bs, err := trimLastLF(buf) + if err != nil { + return "", err + } + return string(bs), nil +} + +func (r *Resp3Parser) intBeforeLF() (int, error) { + buf, err := r.reader.ReadBytes(LF) + if err != nil { + return 0, err + } + bs, err := trimLastLF(buf) + if err != nil { + return 0, err + } + s := string(bs) + i, err := strconv.Atoi(s) + if err != nil { + return 0, &ErrCastFailedToInt{Val: s} + } + return i, nil +} + +func (r *Resp3Parser) readLengthBytesWithLF(length int) ([]byte, error) { + if length == 0 { + if b, err := r.reader.ReadByte(); err != nil { + return nil, err + } else if b != LF { + return nil, &ErrUnexpectString{Str: ""} + } + return nil, nil + } + + buf := make([]byte, length+1) + n, err := r.reader.Read(buf) + if err != nil { + return nil, err + } else if n < length+1 { + return nil, &ErrUnexpectedLineEnd{} + } + + return trimLastLF(buf) +} + +func trimLastLF(buf []byte) ([]byte, error) { + bufLen := len(buf) + if len(buf) == 0 || buf[bufLen-1] != LF { + return nil, &ErrUnexpectedLineEnd{} + } + + return buf[:bufLen-1], nil +} diff --git a/internal/protcl/resp3_test.go b/internal/protcl/resp3_test.go new file mode 100644 index 0000000..71c3816 --- /dev/null +++ b/internal/protcl/resp3_test.go @@ -0,0 +1,62 @@ +package protcl + +import ( + "bufio" + "strings" + "testing" + + testifyAssert "github.com/stretchr/testify/assert" +) + +func testResp3Parser(t *testing.T, input, expect string) { + assert := testifyAssert.New(t) + + parser := NewResp3Parser(bufio.NewReader(strings.NewReader(input))) + result, err := parser.Parse() + assert.Nil(err) + assert.Equal(expect, result.String()) +} + +func testResp3Error(t *testing.T, input, e string) { + assert := testifyAssert.New(t) + + parser := NewResp3Parser(bufio.NewReader(strings.NewReader(input))) + result, err := parser.Parse() + assert.NotNil(err) + assert.Nil(result) + assert.Equal(e, err.Error()) +} + +func TestResp3Parser(t *testing.T) { + // simple string + testResp3Error(t, "+string", `EOF`) + testResp3Parser(t, "+string\n", `"string"`) + + // blob string + testResp3Parser(t, "$0\n\n", `""`) + testResp3Error(t, "$1\n\n", `unexpected line end`) + testResp3Error(t, "$1\naa\n", `unexpected line end`) + testResp3Parser(t, "$10\n1234567890\n", `"1234567890"`) + + // simple error + testResp3Parser(t, "-error\n", `(error) error`) + testResp3Parser(t, "-Err error\n", `(error) Err error`) + + // blob error + testResp3Parser(t, "!3\nerr\n", `(error) err`) + testResp3Parser(t, "!17\nErr this is error\n", `(error) Err this is error`) + + // number + testResp3Parser(t, ":-1\n", `(integer) -1`) + testResp3Parser(t, ":0\n", `(integer) 0`) + testResp3Parser(t, ":100\n", `(integer) 100`) + + // null + testResp3Parser(t, "_\n", "(null)") + + // boolean + testResp3Parser(t, "#t\n", `(boolean) true`) + testResp3Parser(t, "#f\n", `(boolean) false`) + testResp3Error(t, "#x\n", `unexpect string: t/f`) + testResp3Error(t, "#\n", `unexpected line end`) +}