1 module tinyredis.parser;
2 
3 /**
4  * Authors: Adil Baig, adil.baig@aidezigns.com
5  */
6 
7 private:
8     import std.conv  : to;
9     import std.range : isInputRange, isForwardRange, isBidirectionalRange, retro;
10     
11 public : 
12 
13 	import tinyredis.response;
14 
15     /**
16      * Parse a byte stream into a Response struct. 
17      *
18      * The parser works to identify a minimum complete Response. If successful, it removes that chunk from "mb" and returns a Response struct.
19      * On failure it returns a ResponseType.Invalid Response and leaves "mb" untouched. 
20      */
21     @trusted Response parseResponse(ref byte[] mb)
22     {
23         Response response;
24         response.type = ResponseType.Invalid;
25         
26         if(mb.length < 4)
27             return response;
28             
29         char type = mb[0];
30 
31         byte[] bytes;
32         if(!getData(mb[1 .. $], bytes)) //This could be an int value (:), a bulk byte length ($), a status message (+) or an error value (-)
33             return response;
34             
35         size_t tpos = 1 + bytes.length;
36         
37         if(tpos + 2 > mb.length)
38             return response;
39         else
40             tpos += 2; //for "\r\n"
41         
42         switch(type)
43         {
44              case '+' : 
45                 response.type = ResponseType.Status;
46                 response.value = cast(string)bytes;
47                 break;
48                 
49             case '-' :
50                 throw new RedisResponseException(cast(string)bytes);
51 //                break;
52                 
53             case ':' :
54                 response.type = ResponseType.Integer;
55                 response.intval = to!long(cast(char[])bytes);
56                 break;
57                 
58             case '$' :
59                 int l = to!int(cast(char[])bytes);
60                 if(l == -1)
61                 {
62                     response.type = ResponseType.Nil;
63                     break;
64                 }
65                 
66                 if(tpos + l >= mb.length) //We don't have enough data. Let's return an invalid response.
67                     return response;
68                 else
69                 {
70                     response.value = cast(string)mb[tpos .. tpos + l];
71                     tpos += l;
72                         
73                     if(tpos + 2 > mb.length)
74                         return response;
75                     else
76                         tpos += 2;
77                 }
78                 
79                 response.type = ResponseType.Bulk;
80                 break;
81             
82             case '*' :
83             	int l = to!int(cast(char[])bytes);
84                 if(l == -1)
85                 {
86                     response.type = ResponseType.Nil;
87                     break;
88                 }
89                 
90                 response.type = ResponseType.MultiBulk;
91                 response.count = l;
92                  
93                 break;
94                 
95             default :
96                 return response;
97         }
98         
99         mb = mb[tpos .. $];
100         return response;
101     }
102     
103     
104     
105     /* ----------- EXCEPTIONS ------------- */
106     
107     class RedisResponseException : Exception {
108         this(string msg) { super(msg); }
109     }
110     
111     
112 private :
113     @safe pure bool getData(const(byte[]) mb, ref byte[] data)
114     {
115         foreach(p, byte c; mb)
116             if(c == 13) //'\r' 
117                 return true;
118             else
119                 data ~= c;
120 
121         return false;
122     }
123     
124     
125 unittest
126 {
127     //Test Nil bulk
128     byte[] stream = cast(byte[])"$-1\r\n";
129     auto response = parseResponse(stream);
130     assert(response.toString == "");
131     assert(response.toBool == false);
132     assert(cast(bool)response == false);
133     try{
134         cast(int)response;
135         assert(false);
136     }catch(RedisCastException e)
137     {
138         assert(true);
139     }
140     
141     //Test Nil multibulk
142     stream = cast(byte[])"*-1\r\n";
143     response = parseResponse(stream);
144     assert(response.toString == "");
145     assert(response.toBool == false);
146     assert(cast(bool)response == false);
147     try{
148         cast(int)response;
149         assert(false);
150     }catch(RedisCastException e)
151     {
152         assert(true);
153     }
154     
155     //Empty Bulk
156     stream = cast(byte[])"$0\r\n\r\n";
157     response = parseResponse(stream);
158     assert(response.toString == "");
159     assert(response.toBool == false);
160     assert(cast(bool)response == false);
161     
162     stream = cast(byte[])"*4\r\n$3\r\nGET\r\n$1\r\n*\r\n:123\r\n+A Status Message\r\n";
163     
164     response = parseResponse(stream);
165     assert(response.type == ResponseType.MultiBulk);
166     assert(response.count == 4);
167     assert(response.values.length == 0);
168     
169     response = parseResponse(stream);
170     assert(response.type == ResponseType.Bulk);
171     assert(response.value == "GET");
172     assert(cast(string)response == "GET");
173     
174     response = parseResponse(stream);
175     assert(response.type == ResponseType.Bulk);
176     assert(response.value == "*");
177     assert(cast(bool)response == true);
178     
179     response = parseResponse(stream);
180     assert(response.type == ResponseType.Integer);
181     assert(response.intval == 123);
182     assert(cast(string)response == "123");
183     assert(cast(int)response == 123);
184     
185     response = parseResponse(stream);
186     assert(response.type == ResponseType.Status);
187     assert(response.value == "A Status Message");
188     assert(cast(string)response == "A Status Message");
189     try{
190         cast(int)response;
191     }catch(RedisCastException e)
192     {
193         //Exception caught
194     }
195 
196     //Stream should have been used up, verify
197     assert(stream.length == 0);
198     assert(parseResponse(stream).type == ResponseType.Invalid);
199 
200     //Long overflow checking
201     stream = cast(byte[])":9223372036854775808\r\n";
202     try{
203         parseResponse(stream);
204         assert(false, "Tried to convert long.max+1 to long");
205     }
206     catch(ConvOverflowException e){}
207     
208     Response r = {type : ResponseType.Bulk, value : "9223372036854775807"};
209     try{
210         r.toInt(); //Default int
211         assert(false, "Tried to convert long.max to int");
212     }
213     catch(ConvOverflowException e)
214     {
215         //Ok, exception thrown as expected
216     }
217     
218     r.value = "127";
219     assert(r.toInt!byte() == 127); 
220     assert(r.toInt!short() == 127); 
221     assert(r.toInt!int() == 127); 
222     assert(r.toInt!long() == 127); 
223     
224     stream = cast(byte[])"*0\r\n";
225     response = parseResponse(stream);
226     assert(response.count == 0);
227     assert(response.values.length == 0);
228     assert(response.values == []);
229     assert(response.toString == "[]");
230     assert(response.toBool == false);
231     assert(cast(bool)response == false);
232     try{
233         cast(int)response;
234     }catch(RedisCastException e)
235     {
236         assert(true);
237     }
238     
239     //Testing opApply
240     stream = cast(byte[])"*0\r\n";
241     response = parseResponse(stream);
242     foreach(k, v; response)
243         assert(false, "opApply is broken");
244     foreach(v; response)
245         assert(false, "opApply is broken");
246     
247     stream = cast(byte[])"$2\r\n$2\r\n";
248     response = parseResponse(stream);
249     foreach(k, v; response)
250         assert(false, "opApply is broken");
251     foreach(v; response)
252         assert(false, "opApply is broken");
253         
254     stream = cast(byte[])":1000\r\n";
255     response = parseResponse(stream);
256     foreach(k, v; response)
257         assert(false, "opApply is broken");
258     foreach(v; response)
259         assert(false, "opApply is broken");
260         
261     //Testing opApplyReverse
262     stream = cast(byte[])"*0\r\n";
263     response = parseResponse(stream);
264     foreach_reverse(k, v; response)
265         assert(false, "opApplyReverse is broken");
266     foreach_reverse(v; response)
267         assert(false, "opApplyReverse is broken");
268    
269         
270     //Testing ranges for Response
271     assert(isInputRange!Response);
272     assert(isForwardRange!Response);
273     assert(isBidirectionalRange!Response);
274 }