xref: /haiku/src/kits/shared/Json.cpp (revision 22440f4105cafc95cc1d49f9bc65bb395c527d86)
1 /*
2  * Copyright 2014, Augustin Cavalier (waddlesplash)
3  * Copyright 2014, Stephan Aßmus <superstippi@gmx.de>
4  * Distributed under the terms of the MIT License.
5  */
6 
7 
8 #include <Json.h>
9 
10 #include <stdio.h>
11 #include <stdlib.h>
12 
13 #include <MessageBuilder.h>
14 #include <UnicodeChar.h>
15 
16 
17 // #pragma mark - Public methods
18 
19 namespace BPrivate {
20 
21 
22 class ParseException {
23 public:
24 	ParseException(int32 position, BString error)
25 		:
26 		fPosition(position),
27 		fError(error),
28 		fReturnCode(B_BAD_DATA)
29 	{
30 	}
31 
32 	ParseException(int32 position, status_t returnCode)
33 		:
34 		fPosition(position),
35 		fError(""),
36 		fReturnCode(returnCode)
37 	{
38 	}
39 
40 	void PrintToStream() const {
41 		const char* error;
42 		if (fError.Length() > 0)
43 			error = fError.String();
44 		else
45 			error = strerror(fReturnCode);
46 		printf("Parse error at %" B_PRIi32 ": %s\n", fPosition, error);
47 	}
48 
49 	status_t ReturnCode() const
50 	{
51 		return fReturnCode;
52 	}
53 
54 private:
55 	int32		fPosition;
56 	BString		fError;
57 	status_t	fReturnCode;
58 };
59 
60 
61 status_t
62 BJson::Parse(BMessage& message, const char* JSON)
63 {
64 	BString temp(JSON);
65 	return Parse(message, temp);
66 }
67 
68 
69 status_t
70 BJson::Parse(BMessage& message, BString& JSON)
71 {
72 	try {
73 		_Parse(message, JSON);
74 		return B_OK;
75 	} catch (ParseException e) {
76 		e.PrintToStream();
77 		return e.ReturnCode();
78 	}
79 	return B_ERROR;
80 }
81 
82 
83 // #pragma mark - Private methods
84 
85 
86 void
87 BJson::_Parse(BMessage& message, BString& JSON)
88 {
89 	BMessageBuilder builder(message);
90 	int32 pos = 0;
91 	int32 length = JSON.Length();
92 
93 	/* Locals used by the parser. */
94 	// Keeps track of the hierarchy (e.g. "{[{{") that has
95 	// been read in. Allows the parser to verify that openbraces
96 	// match up to closebraces and so on and so forth.
97 	BString hierarchy("");
98 	// Stores the key that was just read by the string parser,
99 	// in the case that we are parsing a map.
100 	BString key("");
101 
102 	// TODO: Check builder return codes and throw exception, or
103 	// change builder implementation/interface to throw exceptions
104 	// instead of returning errors.
105 	// TODO: Elimitate more duplicated code, for example by moving
106 	// more code into _ParseConstant().
107 
108 	while (pos < length) {
109 		switch (JSON[pos]) {
110 		case '{':
111 			hierarchy += "{";
112 
113 			if (hierarchy != "{") {
114 				if (builder.What() == JSON_TYPE_ARRAY)
115 					builder.PushObject(builder.CountNames());
116 				else {
117 					builder.PushObject(key.String());
118 					key = "";
119 				}
120 			}
121 
122 			builder.SetWhat(JSON_TYPE_MAP);
123 			break;
124 
125 		case '}':
126 			if (hierarchy.EndsWith("{") && hierarchy.Length() != 1) {
127 				hierarchy.Truncate(hierarchy.Length() - 1);
128 				builder.PopObject();
129 			} else if (hierarchy.Length() == 1)
130 				return; // End of the JSON data
131 			else
132 				throw ParseException(pos, "Unmatched closebrace }");
133 
134             break;
135 
136 		case '[':
137 			hierarchy += "[";
138 
139 			if (builder.What() == JSON_TYPE_ARRAY)
140 				builder.PushObject(builder.CountNames());
141 			else {
142 				builder.PushObject(key.String());
143 				key = "";
144 			}
145 
146 			builder.SetWhat(JSON_TYPE_ARRAY);
147 			break;
148 
149 		case ']':
150 			if (hierarchy.EndsWith("[")) {
151 				hierarchy.Truncate(hierarchy.Length() - 1);
152 				builder.PopObject();
153 			} else {
154 				BString error("Unmatched closebrace ] hierarchy: ");
155 				error << hierarchy;
156 				throw ParseException(pos, error);
157 			}
158 
159 			break;
160 
161 		case 't':
162 		{
163 			if (builder.What() != JSON_TYPE_ARRAY && key.Length() == 0) {
164 				throw ParseException(pos,
165 					"'true' cannot be a key, it can only be a value");
166 			}
167 
168 			if (_ParseConstant(JSON, pos, "true")) {
169 				if (builder.What() == JSON_TYPE_ARRAY)
170 					key.SetToFormat("%" B_PRIu32, builder.CountNames());
171 				builder.AddBool(key.String(), true);
172 				key = "";
173 			} else
174 				throw ParseException(pos, "Unexpected 't'");
175 
176 			break;
177 		}
178 
179 		case 'f':
180 		{
181 			if (builder.What() != JSON_TYPE_ARRAY && key.Length() == 0) {
182 				throw ParseException(pos,
183 					"'false' cannot be a key, it can only be a value");
184 			}
185 
186 			if (_ParseConstant(JSON, pos, "false")) {
187 				if (builder.What() == JSON_TYPE_ARRAY)
188 					key.SetToFormat("%" B_PRIu32, builder.CountNames());
189 				builder.AddBool(key.String(), false);
190 				key = "";
191 			} else
192 				throw ParseException(pos, "Unexpected 'f'");
193 
194 			break;
195 		}
196 
197         case 'n':
198         {
199 			if (builder.What() != JSON_TYPE_ARRAY && key.Length() == 0) {
200 				throw ParseException(pos,
201 					"'null' cannot be a key, it can only be a value");
202 			}
203 
204 			if (_ParseConstant(JSON, pos, "null")) {
205 				if (builder.What() == JSON_TYPE_ARRAY)
206 					key.SetToFormat("%" B_PRIu32, builder.CountNames());
207 				builder.AddPointer(key.String(), (void*)NULL);
208 				key = "";
209 			} else
210 				throw ParseException(pos, "Unexpected 'n'");
211 
212 			break;
213 		}
214 
215 		case '"':
216 			if (builder.What() != JSON_TYPE_ARRAY && key.Length() == 0)
217 				key = _ParseString(JSON, pos);
218 			else if (builder.What() != JSON_TYPE_ARRAY && key.Length() > 0) {
219 				builder.AddString(key, _ParseString(JSON, pos));
220 				key = "";
221 			} else if (builder.What() == JSON_TYPE_ARRAY) {
222 				key << builder.CountNames();
223 				builder.AddString(key, _ParseString(JSON, pos));
224 				key = "";
225 			} else
226 				throw ParseException(pos, "Internal error at encountering \"");
227 
228 			break;
229 
230 		case '+':
231 		case '-':
232 		case '0':
233 		case '1':
234 		case '2':
235 		case '3':
236 		case '4':
237 		case '5':
238 		case '6':
239 		case '7':
240 		case '8':
241 		case '9':
242 		{
243 			if (builder.What() != JSON_TYPE_ARRAY && key.Length() == 0) {
244 				throw ParseException(pos,
245 					"Numbers cannot be keys, they can only be values");
246 			}
247 
248 			if (builder.What() == JSON_TYPE_ARRAY)
249 				key << builder.CountNames();
250 
251 			double number = _ParseNumber(JSON, pos);
252 			builder.AddDouble(key.String(), number);
253 
254 			key = "";
255 			break;
256 		}
257 
258 		case ':':
259 		case ',':
260 		default:
261 			// No need to do anything here.
262 			break;
263 		}
264 		pos++;
265 	}
266 
267 	throw ParseException(pos, "Unexpected end of document");
268 }
269 
270 
271 BString
272 BJson::_ParseString(BString& JSON, int32& pos)
273 {
274 	if (JSON[pos] != '"') // Verify we're at the start of a string.
275 		return BString("");
276 	pos++;
277 
278 	BString str;
279 	while (JSON[pos] != '"') {
280 		if (JSON[pos] == '\\') {
281 			pos++;
282 			switch (JSON[pos]) {
283 			case 'b':
284 				str += "\b";
285 				break;
286 
287 			case 'f':
288 				str += "\f";
289 				break;
290 
291 			case 'n':
292 				str += "\n";
293 				break;
294 
295 			case 'r':
296 				str += "\r";
297 				break;
298 
299 			case 't':
300 				str += "\t";
301 				break;
302 
303 			case 'u': // 4-byte hexadecimal Unicode char (e.g. "\uffff")
304 			{
305 				uint intValue;
306 				BString substr;
307 				JSON.CopyInto(substr, pos + 1, 4);
308 				if (sscanf(substr.String(), "%4x", &intValue) != 1)
309 					return str;
310 					// We probably hit the end of the string.
311 					// This probably should be counted as an error,
312 					// but for now let's soft-fail instead of hard-fail.
313 
314 				char character[20];
315 				char* ptr = character;
316 				BUnicodeChar::ToUTF8(intValue, &ptr);
317 				str.AppendChars(character, 1);
318 				pos += 4;
319 				break;
320 			}
321 
322 			default:
323 				str += JSON[pos];
324 				break;
325 			}
326 		} else
327 			str += JSON[pos];
328 		pos++;
329 	}
330 
331 	return str;
332 }
333 
334 
335 double
336 BJson::_ParseNumber(BString& JSON, int32& pos)
337 {
338 	BString value;
339 
340 	while (true) {
341 		switch (JSON[pos]) {
342 		case '+':
343 		case '-':
344 		case 'e':
345 		case 'E':
346 		case '0':
347 		case '1':
348 		case '2':
349 		case '3':
350 		case '4':
351 		case '5':
352 		case '6':
353 		case '7':
354 		case '8':
355 		case '9':
356 		case '.':
357 			value += JSON[pos];
358 			pos++;
359 			continue;
360 
361 		default:
362 			// We've reached the end of the number, so decrement the
363 			// "pos" value so that the parser picks up on the next char.
364 			pos--;
365 			break;
366 		}
367 		break;
368 	}
369 
370 	return strtod(value.String(), NULL);
371 }
372 
373 
374 bool
375 BJson::_ParseConstant(BString& JSON, int32& pos, const char* constant)
376 {
377 	BString value;
378 	JSON.CopyInto(value, pos, strlen(constant));
379 	if (value == constant) {
380 		// Increase pos by the remainder of the constant, pos will be
381 		// increased for the first letter in the main parse loop.
382 		pos += strlen(constant) - 1;
383 		return true;
384 	} else
385 		return false;
386 }
387 
388 
389 } // namespace BPrivate
390 
391