diff --git a/resp.go b/resp.go new file mode 100644 index 0000000..5752188 --- /dev/null +++ b/resp.go @@ -0,0 +1,129 @@ +package redcon + +import ( + "strconv" +) + +// Type of RESP +type Type byte + +// Various RESP kinds +const ( + Integer = ':' + String = '+' + Bulk = '$' + Array = '*' + Error = '-' +) + +// RESP ... +type RESP struct { + Type Type + Raw []byte + Data []byte + Count int +} + +// ForEach iterates over each Array element +func (r *RESP) ForEach(iter func(resp RESP) bool) { + data := r.Data + for i := 0; i < r.Count; i++ { + n, resp := ReadNextRESP(data) + if !iter(resp) { + return + } + data = data[n:] + } +} + +// ReadNextRESP returns the next resp in b and returns the number of bytes the +// took up the result. +func ReadNextRESP(b []byte) (n int, resp RESP) { + if len(b) == 0 { + return 0, RESP{} // no data to read + } + resp.Type = Type(b[0]) + switch resp.Type { + case Integer, String, Bulk, Array, Error: + default: + return 0, RESP{} // invalid kind + } + // read to end of line + i := 1 + for ; ; i++ { + if i == len(b) { + return 0, RESP{} // not enough data + } + if b[i] == '\n' { + if b[i-1] != '\r' { + return 0, RESP{} //, missing CR character + } + i++ + break + } + } + resp.Raw = b[0:i] + resp.Data = b[1 : i-2] + if resp.Type == Integer { + // Integer + if len(resp.Data) == 0 { + return 0, RESP{} //, invalid integer + } + var j int + if resp.Data[0] == '-' { + if len(resp.Data) == 1 { + return 0, RESP{} //, invalid integer + } + j++ + } + for ; j < len(resp.Data); j++ { + if resp.Data[j] < '0' || resp.Data[j] > '9' { + return 0, RESP{} // invalid integer + } + } + return len(resp.Raw), resp + } + if resp.Type == String || resp.Type == Error { + // String, Error + return len(resp.Raw), resp + } + var err error + resp.Count, err = strconv.Atoi(string(resp.Data)) + if resp.Type == Bulk { + // Bulk + if err != nil { + return 0, RESP{} // invalid number of bytes + } + if resp.Count < 0 { + resp.Data = nil + resp.Count = 0 + return len(resp.Raw), resp + } + if len(b) < i+resp.Count+2 { + return 0, RESP{} // not enough data + } + if b[i+resp.Count] != '\r' || b[i+resp.Count+1] != '\n' { + return 0, RESP{} // invalid end of line + } + resp.Data = b[i : i+resp.Count] + resp.Raw = b[0 : i+resp.Count+2] + resp.Count = 0 + return len(resp.Raw), resp + } + // Array + if err != nil { + return 0, RESP{} // invalid number of elements + } + var tn int + sdata := b[i:] + for j := 0; j < resp.Count; j++ { + rn, rresp := ReadNextRESP(sdata) + if rresp.Type == 0 { + return 0, RESP{} + } + tn += rn + } + resp.Data = b[i : i+tn] + resp.Raw = b[0 : i+tn] + return len(resp.Raw), resp +} diff --git a/resp_test.go b/resp_test.go new file mode 100644 index 0000000..cf45f56 --- /dev/null +++ b/resp_test.go @@ -0,0 +1,130 @@ +package redcon + +import ( + "fmt" + "strconv" + "testing" +) + +func isEmptyRESP(resp RESP) bool { + return resp.Type == 0 && resp.Count == 0 && + resp.Data == nil && resp.Raw == nil +} + +func expectBad(t *testing.T, payload string) { + t.Helper() + n, resp := ReadNextRESP([]byte(payload)) + if n > 0 || !isEmptyRESP(resp) { + t.Fatalf("expected empty resp") + } +} + +func respVOut(a RESP) string { + var data string + var raw string + if a.Data == nil { + data = "nil" + } else { + data = strconv.Quote(string(a.Data)) + } + if a.Raw == nil { + raw = "nil" + } else { + raw = strconv.Quote(string(a.Raw)) + } + return fmt.Sprintf("{Type: %d, Count: %d, Data: %s, Raw: %s}", + a.Type, a.Count, data, raw, + ) +} + +func respEquals(a, b RESP) bool { + if a.Count != b.Count { + return false + } + if a.Type != b.Type { + return false + } + if (a.Data == nil && b.Data != nil) || (a.Data != nil && b.Data == nil) { + return false + } + if string(a.Data) != string(b.Data) { + return false + } + if (a.Raw == nil && b.Raw != nil) || (a.Raw != nil && b.Raw == nil) { + return false + } + if string(a.Raw) != string(b.Raw) { + return false + } + return true +} + +func expectGood(t *testing.T, payload string, exp RESP) { + t.Helper() + n, resp := ReadNextRESP([]byte(payload)) + if n != len(payload) || isEmptyRESP(resp) { + t.Fatalf("expected good resp") + } + if string(resp.Raw) != payload { + t.Fatalf("expected '%s', got '%s'", payload, resp.Raw) + } + exp.Raw = []byte(payload) + switch exp.Type { + case Integer, String, Error: + exp.Data = []byte(payload[1 : len(payload)-2]) + } + if !respEquals(resp, exp) { + t.Fatalf("expected %v, got %v", respVOut(exp), respVOut(resp)) + } +} + +func TestRESP(t *testing.T) { + expectBad(t, "") + expectBad(t, "^hello\r\n") + expectBad(t, "+hello\r") + expectBad(t, "+hello\n") + expectBad(t, ":\r\n") + expectBad(t, ":-\r\n") + expectBad(t, ":-abc\r\n") + expectBad(t, ":abc\r\n") + expectGood(t, ":-123\r\n", RESP{Type: Integer}) + expectGood(t, ":123\r\n", RESP{Type: Integer}) + expectBad(t, "+\r") + expectBad(t, "+\n") + expectGood(t, "+\r\n", RESP{Type: String}) + expectGood(t, "+hello world\r\n", RESP{Type: String}) + expectBad(t, "-\r") + expectBad(t, "-\n") + expectGood(t, "-\r\n", RESP{Type: Error}) + expectGood(t, "-hello world\r\n", RESP{Type: Error}) + expectBad(t, "$") + expectBad(t, "$\r") + expectBad(t, "$\r\n") + expectGood(t, "$-1\r\n", RESP{Type: Bulk}) + expectGood(t, "$0\r\n\r\n", RESP{Type: Bulk, Data: []byte("")}) + expectBad(t, "$5\r\nhello\r") + expectBad(t, "$5\r\nhello\n\n") + expectGood(t, "$5\r\nhello\r\n", RESP{Type: Bulk, Data: []byte("hello")}) + expectBad(t, "*a\r\n") + expectBad(t, "*3\r\n") + expectBad(t, "*3\r\n:hello\r") + expectGood(t, "*3\r\n:1\r\n:2\r\n:3\r\n", + RESP{Type: Array, Count: 3, Data: []byte(":1\r\n:2\r\n:3\r\n")}) + + var xx int + _, r := ReadNextRESP([]byte("*4\r\n:1\r\n:2\r\n:3\r\n:4\r\n")) + r.ForEach(func(resp RESP) bool { + xx++ + x, _ := strconv.Atoi(string(resp.Data)) + if x != xx { + t.Fatalf("expected %v, got %v", x, xx) + } + if xx == 3 { + return false + } + return true + }) + if xx != 3 { + t.Fatalf("expected %v, got %v", 3, xx) + } +}