xref: /haiku/src/kits/shared/Json.cpp (revision 44b874666af6b25da6311e5a1c506ee3761f4268)
1 /*
2  * Copyright 2017, Andrew Lindesay <apl@lindesay.co.nz>
3  * Copyright 2014-2017, Augustin Cavalier (waddlesplash)
4  * Copyright 2014, Stephan Aßmus <superstippi@gmx.de>
5  * Distributed under the terms of the MIT License.
6  */
7 
8 
9 #include "Json.h"
10 
11 #include <cstdio>
12 #include <cstdlib>
13 #include <ctype.h>
14 #include <cerrno>
15 
16 #include <AutoDeleter.h>
17 #include <DataIO.h>
18 #include <UnicodeChar.h>
19 
20 #include "JsonEventListener.h"
21 #include "JsonMessageWriter.h"
22 
23 
24 // #pragma mark - Public methods
25 
26 namespace BPrivate {
27 
28 
29 static bool
30 b_jsonparse_is_hex(char c)
31 {
32 	return isdigit(c)
33 		|| (c > 0x41 && c <= 0x46)
34 		|| (c > 0x61 && c <= 0x66);
35 }
36 
37 
38 static bool
39 b_jsonparse_all_hex(const char* c)
40 {
41 	for (int i = 0; i < 4; i++) {
42 		if (!b_jsonparse_is_hex(c[i]))
43 			return false;
44 	}
45 
46 	return true;
47 }
48 
49 
50 /*! This class carries state around the parsing process. */
51 
52 class JsonParseContext {
53 public:
54 	JsonParseContext(BDataIO* data, BJsonEventListener* listener)
55 		:
56 		fListener(listener),
57 		fData(data),
58 		fLineNumber(1), // 1 is the first line
59 		fPushbackChar(0),
60 		fHasPushbackChar(false)
61 	{
62 	}
63 
64 
65 	BJsonEventListener* Listener() const
66 	{
67 		return fListener;
68 	}
69 
70 
71 	BDataIO* Data() const
72 	{
73 		return fData;
74 	}
75 
76 
77 	int LineNumber() const
78 	{
79 		return fLineNumber;
80 	}
81 
82 
83 	void IncrementLineNumber()
84 	{
85 		fLineNumber++;
86 	}
87 
88 
89 		// TODO; there is considerable opportunity for performance improvements
90 		// here by buffering the input and then feeding it into the parse
91 		// algorithm character by character.
92 
93 	status_t NextChar(char* buffer)
94 	{
95 
96 		if (fHasPushbackChar) {
97 			buffer[0] = fPushbackChar;
98 			fHasPushbackChar = false;
99 			return B_OK;
100 		}
101 
102 		return Data()->ReadExactly(buffer, 1);
103 	}
104 
105 
106 	void PushbackChar(char c)
107 	{
108 		fPushbackChar = c;
109 		fHasPushbackChar = true;
110 	}
111 
112 private:
113 	BJsonEventListener*		fListener;
114 	BDataIO*				fData;
115 	uint32					fLineNumber;
116 	char					fPushbackChar;
117 	bool					fHasPushbackChar;
118 };
119 
120 
121 status_t
122 BJson::Parse(const BString& JSON, BMessage& message)
123 {
124 	return Parse(JSON.String(), message);
125 }
126 
127 
128 status_t
129 BJson::Parse(const char* JSON, BMessage& message)
130 {
131 	BMemoryIO* input = new BMemoryIO(JSON, strlen(JSON));
132 	ObjectDeleter<BMemoryIO> inputDeleter(input);
133 	BJsonMessageWriter* writer = new BJsonMessageWriter(message);
134 	ObjectDeleter<BJsonMessageWriter> writerDeleter(writer);
135 
136 	Parse(input, writer);
137 	status_t result = writer->ErrorStatus();
138 
139 	return result;
140 }
141 
142 
143 /*! The data is read as a stream of JSON data.  As the JSON is read, events are
144     raised such as;
145      - string
146      - number
147      - true
148      - array start
149      - object end
150     Each event is sent to the listener to process as required.
151 */
152 
153 void
154 BJson::Parse(BDataIO* data, BJsonEventListener* listener)
155 {
156 	JsonParseContext context(data, listener);
157 	ParseAny(context);
158 	listener->Complete();
159 }
160 
161 
162 // #pragma mark - Specific parse logic.
163 
164 
165 bool
166 BJson::NextChar(JsonParseContext& jsonParseContext, char* c)
167 {
168 	status_t result = jsonParseContext.NextChar(c);
169 
170 	switch (result) {
171 		case B_OK:
172 			return true;
173 
174 		case B_PARTIAL_READ:
175 		{
176 			jsonParseContext.Listener()->HandleError(B_BAD_DATA,
177 				jsonParseContext.LineNumber(), "unexpected end of input");
178 			return false;
179 		}
180 
181 		default:
182 		{
183 			jsonParseContext.Listener()->HandleError(result, -1,
184 				"io related read error");
185 			return false;
186 		}
187 	}
188 }
189 
190 
191 bool
192 BJson::NextNonWhitespaceChar(JsonParseContext& jsonParseContext, char* c)
193 {
194 	while (true) {
195 		if (!NextChar(jsonParseContext, c))
196 			return false;
197 
198 		switch (*c) {
199 			case 0x0a: // newline
200 			case 0x0d: // cr
201 				jsonParseContext.IncrementLineNumber();
202 			case ' ': // space
203 					// swallow whitespace as it is not syntactically
204 					// significant.
205 				break;
206 
207 			default:
208 				return true;
209 		}
210 	}
211 }
212 
213 
214 bool
215 BJson::ParseAny(JsonParseContext& jsonParseContext)
216 {
217 	char c;
218 
219 	if (!NextNonWhitespaceChar(jsonParseContext, &c))
220 		return false;
221 
222 	switch (c) {
223 		case 'f': // [f]alse
224 			return ParseExpectedVerbatimStringAndRaiseEvent(
225 				jsonParseContext, "alse", 4, 'f', B_JSON_FALSE);
226 
227 		case 't': // [t]rue
228 			return ParseExpectedVerbatimStringAndRaiseEvent(
229 				jsonParseContext, "rue", 3, 't', B_JSON_TRUE);
230 
231 		case 'n': // [n]ull
232 			return ParseExpectedVerbatimStringAndRaiseEvent(
233 				jsonParseContext, "ull", 3, 'n', B_JSON_NULL);
234 
235 		case '"':
236 			return ParseString(jsonParseContext, B_JSON_STRING);
237 
238 		case '{':
239 			return ParseObject(jsonParseContext);
240 
241 		case '[':
242 			return ParseArray(jsonParseContext);
243 
244 		case '+':
245 		case '-':
246 		case '0':
247 		case '1':
248 		case '2':
249 		case '3':
250 		case '4':
251 		case '5':
252 		case '6':
253 		case '7':
254 		case '8':
255 		case '9':
256 			jsonParseContext.PushbackChar(c); // keeps the parse simple
257 			return ParseNumber(jsonParseContext);
258 
259 		default:
260 		{
261 			BString errorMessage;
262 			if (c >= 0x20 && c < 0x7f) {
263 				errorMessage.SetToFormat("unexpected character [%" B_PRIu8 "]"
264 					" (%c) when parsing element", static_cast<uint8>(c), c);
265 			} else {
266 				errorMessage.SetToFormat("unexpected character [%" B_PRIu8 "]"
267 					" when parsing element", (uint8) c);
268 			}
269 			jsonParseContext.Listener()->HandleError(B_BAD_DATA,
270 				jsonParseContext.LineNumber(), errorMessage.String());
271 			return false;
272 		}
273 	}
274 
275 	return true;
276 }
277 
278 
279 /*! This method captures an object name, a separator ':' and then any value. */
280 
281 bool
282 BJson::ParseObjectNameValuePair(JsonParseContext& jsonParseContext)
283 {
284 	bool didParseName = false;
285 	char c;
286 
287 	while (true) {
288 		if (!NextNonWhitespaceChar(jsonParseContext, &c))
289 			return false;
290 
291 		switch (c) {
292 			case '\"': // name of the object
293 			{
294 				if (!didParseName) {
295 					if (!ParseString(jsonParseContext, B_JSON_OBJECT_NAME))
296 						return false;
297 
298 					didParseName = true;
299 				} else {
300 					jsonParseContext.Listener()->HandleError(B_BAD_DATA,
301 						jsonParseContext.LineNumber(), "unexpected"
302 							" [\"] character when parsing object name-"
303 							" value separator");
304 					return false;
305 				}
306 				break;
307 			}
308 
309 			case ':': // separator
310 			{
311 				if (didParseName) {
312 					if (!ParseAny(jsonParseContext))
313 						return false;
314 					return true;
315 				} else {
316 					jsonParseContext.Listener()->HandleError(B_BAD_DATA,
317 						jsonParseContext.LineNumber(), "unexpected"
318 							" [:] character when parsing object name-"
319 							" value pair");
320 					return false;
321 				}
322 			}
323 
324 			default:
325 			{
326 				BString errorMessage;
327 				errorMessage.SetToFormat(
328 					"unexpected character [%c] when parsing object"
329 					" name-value pair",
330 					c);
331 				jsonParseContext.Listener()->HandleError(B_BAD_DATA,
332 					jsonParseContext.LineNumber(), errorMessage.String());
333 				return false;
334 			}
335 		}
336 	}
337 }
338 
339 
340 bool
341 BJson::ParseObject(JsonParseContext& jsonParseContext)
342 {
343 	if (!jsonParseContext.Listener()->Handle(
344 			BJsonEvent(B_JSON_OBJECT_START))) {
345 		return false;
346 	}
347 
348 	char c;
349 	bool firstItem = true;
350 
351 	while (true) {
352 		if (!NextNonWhitespaceChar(jsonParseContext, &c))
353 			return false;
354 
355 		switch (c) {
356 			case '}': // terminate the object
357 			{
358 				if (!jsonParseContext.Listener()->Handle(
359 						BJsonEvent(B_JSON_OBJECT_END))) {
360 					return false;
361 				}
362 				return true;
363 			}
364 
365 			case ',': // next value.
366 			{
367 				if (firstItem) {
368 					jsonParseContext.Listener()->HandleError(B_BAD_DATA,
369 						jsonParseContext.LineNumber(), "unexpected"
370 							" item separator when parsing start of"
371 							" object");
372 					return false;
373 				}
374 
375 				if (!ParseObjectNameValuePair(jsonParseContext))
376 					return false;
377 				break;
378 			}
379 
380 			default:
381 			{
382 				if (firstItem) {
383 					jsonParseContext.PushbackChar(c);
384 					if (!ParseObjectNameValuePair(jsonParseContext))
385 						return false;
386 					firstItem = false;
387 				} else {
388 					jsonParseContext.Listener()->HandleError(B_BAD_DATA,
389 						jsonParseContext.LineNumber(), "expected"
390 							" separator when parsing an object");
391 				}
392 			}
393 		}
394 	}
395 
396 	return true;
397 }
398 
399 
400 bool
401 BJson::ParseArray(JsonParseContext& jsonParseContext)
402 {
403 	if (!jsonParseContext.Listener()->Handle(
404 			BJsonEvent(B_JSON_ARRAY_START))) {
405 		return false;
406 	}
407 
408 	char c;
409 	bool firstItem = true;
410 
411 	while (true) {
412 		if (!NextNonWhitespaceChar(jsonParseContext, &c))
413 			return false;
414 
415 		switch (c) {
416 			case ']': // terminate the array
417 			{
418 				if (!jsonParseContext.Listener()->Handle(
419 						BJsonEvent(B_JSON_ARRAY_END))) {
420 					return false;
421 				}
422 				return true;
423 			}
424 
425 			case ',': // next value.
426 			{
427 				if (firstItem) {
428 					jsonParseContext.Listener()->HandleError(B_BAD_DATA,
429 						jsonParseContext.LineNumber(), "unexpected"
430 							" item separator when parsing start of"
431 							" array");
432 				}
433 
434 				if (!ParseAny(jsonParseContext))
435 					return false;
436 				break;
437 			}
438 
439 			default:
440 			{
441 				if (firstItem) {
442 					jsonParseContext.PushbackChar(c);
443 					if (!ParseAny(jsonParseContext))
444 						return false;
445 					firstItem = false;
446 				} else {
447 					jsonParseContext.Listener()->HandleError(B_BAD_DATA,
448 						jsonParseContext.LineNumber(), "expected"
449 							" separator when parsing an array");
450 				}
451 			}
452 		}
453 	}
454 
455 	return true;
456 }
457 
458 
459 bool
460 BJson::ParseEscapeUnicodeSequence(JsonParseContext& jsonParseContext,
461 	BString& stringResult)
462 {
463 	char buffer[5];
464 	buffer[4] = 0;
465 
466 	if (!NextChar(jsonParseContext, &buffer[0])
467 		|| !NextChar(jsonParseContext, &buffer[1])
468 		|| !NextChar(jsonParseContext, &buffer[2])
469 		|| !NextChar(jsonParseContext, &buffer[3])) {
470 		return false;
471 	}
472 
473 	if (!b_jsonparse_all_hex(buffer)) {
474 		BString errorMessage;
475 		errorMessage.SetToFormat(
476 			"malformed unicode sequence [%s] in string parsing",
477 			buffer);
478 		jsonParseContext.Listener()->HandleError(B_BAD_DATA,
479 			jsonParseContext.LineNumber(), errorMessage.String());
480 		return false;
481 	}
482 
483 	uint intValue;
484 
485 	if (sscanf(buffer, "%4x", &intValue) != 1) {
486 		BString errorMessage;
487 		errorMessage.SetToFormat(
488 			"unable to process unicode sequence [%s] in string "
489 			" parsing", buffer);
490 		jsonParseContext.Listener()->HandleError(B_BAD_DATA,
491 			jsonParseContext.LineNumber(), errorMessage.String());
492 		return false;
493 	}
494 
495 	char character[7];
496 	char* ptr = character;
497 	BUnicodeChar::ToUTF8(intValue, &ptr);
498 	int32 sequenceLength = ptr - character;
499 	stringResult.Append(character, sequenceLength);
500 
501 	return true;
502 }
503 
504 
505 bool
506 BJson::ParseStringEscapeSequence(JsonParseContext& jsonParseContext,
507 	BString& stringResult)
508 {
509 	char c;
510 
511 	if (!NextChar(jsonParseContext, &c))
512 		return false;
513 
514 	switch (c) {
515 		case 'n':
516 			stringResult += "\n";
517 			break;
518 		case 'r':
519 			stringResult += "\r";
520 			break;
521 		case 'b':
522 			stringResult += "\b";
523 			break;
524 		case 'f':
525 			stringResult += "\f";
526 			break;
527 		case '\\':
528 			stringResult += "\\";
529 			break;
530 		case '/':
531 			stringResult += "/";
532 			break;
533 		case 't':
534 			stringResult += "\t";
535 			break;
536 		case '"':
537 			stringResult += "\"";
538 			break;
539 		case 'u':
540 		{
541 				// unicode escape sequence.
542 			if (!ParseEscapeUnicodeSequence(jsonParseContext,
543 					stringResult)) {
544 				return false;
545 			}
546 			break;
547 		}
548 		default:
549 		{
550 			BString errorMessage;
551 			errorMessage.SetToFormat(
552 				"unexpected escaped character [%c] in string parsing",
553 				c);
554 			jsonParseContext.Listener()->HandleError(B_BAD_DATA,
555 				jsonParseContext.LineNumber(), errorMessage.String());
556 			return false;
557 		}
558 	}
559 
560 	return true;
561 }
562 
563 
564 bool
565 BJson::ParseString(JsonParseContext& jsonParseContext,
566 	json_event_type eventType)
567 {
568 	char c;
569 	BString stringResult;
570 
571 	while(true) {
572 		if (!NextChar(jsonParseContext, &c))
573     		return false;
574 
575 		switch (c) {
576 			case '"':
577 			{
578 					// terminates the string assembled so far.
579 				jsonParseContext.Listener()->Handle(
580 					BJsonEvent(eventType, stringResult.String()));
581 				return true;
582 			}
583 
584 			case '\\':
585 			{
586 				if (!ParseStringEscapeSequence(jsonParseContext,
587 					stringResult)) {
588 					return false;
589 				}
590 				break;
591 			}
592 
593 			default:
594 			{
595 				uint8 uc = static_cast<uint8>(c);
596 
597 				if(uc < 0x20) { // control characters are not allowed
598 					BString errorMessage;
599 					errorMessage.SetToFormat("illegal control character"
600 						" [%" B_PRIu8 "] when parsing a string", uc);
601 					jsonParseContext.Listener()->HandleError(B_BAD_DATA,
602 						jsonParseContext.LineNumber(),
603 						errorMessage.String());
604 					return false;
605 				}
606 
607 				stringResult.Append(&c, 1);
608 				break;
609 			}
610 		}
611 	}
612 }
613 
614 
615 bool
616 BJson::ParseExpectedVerbatimStringAndRaiseEvent(
617 	JsonParseContext& jsonParseContext, const char* expectedString,
618 	size_t expectedStringLength, char leadingChar,
619 	json_event_type jsonEventType)
620 {
621 	if (ParseExpectedVerbatimString(jsonParseContext, expectedString,
622 			expectedStringLength, leadingChar)) {
623 		if (!jsonParseContext.Listener()->Handle(BJsonEvent(jsonEventType)))
624 			return false;
625 	}
626 
627 	return true;
628 }
629 
630 /*! This will make sure that the constant string is available at the input. */
631 
632 bool
633 BJson::ParseExpectedVerbatimString(JsonParseContext& jsonParseContext,
634 	const char* expectedString, size_t expectedStringLength, char leadingChar)
635 {
636 	char c;
637 	size_t offset = 0;
638 
639 	while (offset < expectedStringLength) {
640 		if (!NextChar(jsonParseContext, &c))
641 			return false;
642 
643 		if (c != expectedString[offset]) {
644 			BString errorMessage;
645 			errorMessage.SetToFormat("malformed json primative literal; "
646 				"expected [%c%s], but got [%c] at position %" B_PRIdSSIZE,
647 				leadingChar, expectedString, c, offset);
648 			jsonParseContext.Listener()->HandleError(B_BAD_DATA,
649 				jsonParseContext.LineNumber(), errorMessage.String());
650 			return false;
651 		}
652 
653 		offset++;
654 	}
655 
656 	return true;
657 }
658 
659 
660 /*! This function checks to see that the supplied string is a well formed
661     JSON number.  It does this from a string rather than a stream for
662     convenience.  This is not anticipated to impact performance because
663     the string values are short.
664 */
665 
666 bool
667 BJson::IsValidNumber(BString& number)
668 {
669 	int32 offset = 0;
670 	int32 len = number.Length();
671 
672 	if (offset < len && number[offset] == '-')
673 		offset++;
674 
675 	if (offset >= len)
676 		return false;
677 
678 	if (isdigit(number[offset]) && number[offset] != '0') {
679 		while (offset < len && isdigit(number[offset]))
680 			offset++;
681 	} else {
682 		if (number[offset] == '0')
683 			offset++;
684 		else
685 			return false;
686 	}
687 
688 	if (offset < len && number[offset] == '.') {
689 		offset++;
690 
691 		if (offset >= len)
692 			return false;
693 
694 		while (offset < len && isdigit(number[offset]))
695 			offset++;
696 	}
697 
698 	if (offset < len && (number[offset] == 'E' || number[offset] == 'e')) {
699 		offset++;
700 
701 		if(offset < len && (number[offset] == '+' || number[offset] == '-'))
702 		 	offset++;
703 
704 		if (offset >= len)
705 			return false;
706 
707 		while (offset < len && isdigit(number[offset]))
708 			offset++;
709 	}
710 
711 	return offset == len;
712 }
713 
714 
715 /*! Note that this method hits the 'NextChar' method on the context directly
716     and handles any end-of-file state itself because it is feasible that the
717     entire JSON payload is a number and because (unlike other structures, the
718     number can take the end-of-file to signify the end of the number.
719 */
720 
721 bool
722 BJson::ParseNumber(JsonParseContext& jsonParseContext)
723 {
724 	BString value;
725 
726 	while (true) {
727 		char c;
728 		status_t result = jsonParseContext.NextChar(&c);
729 
730 		switch (result) {
731 			case B_OK:
732 			{
733 				if (isdigit(c)) {
734 					value += c;
735 					break;
736 				}
737 
738 				if (NULL != strchr("+-eE.", c)) {
739 					value += c;
740 					break;
741 				}
742 
743 				jsonParseContext.PushbackChar(c);
744 				// intentional fall through
745 			}
746 			case B_PARTIAL_READ:
747 			{
748 				errno = 0;
749 
750 				if (!IsValidNumber(value)) {
751 					jsonParseContext.Listener()->HandleError(B_BAD_DATA,
752 						jsonParseContext.LineNumber(), "malformed number");
753 					return false;
754 				}
755 
756 				jsonParseContext.Listener()->Handle(BJsonEvent(B_JSON_NUMBER,
757 					value.String()));
758 
759 				return true;
760 			}
761 			default:
762 			{
763 				jsonParseContext.Listener()->HandleError(result, -1,
764 					"io related read error");
765 				return false;
766 			}
767 		}
768 	}
769 }
770 
771 } // namespace BPrivate