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