HaikuDepot: notify work status from main window
[haiku.git] / src / kits / shared / Json.cpp
blob9e34d234daf159d70e20d09a604275f716a9b07d
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 */
9 #include "Json.h"
11 #include <cstdio>
12 #include <cstdlib>
13 #include <ctype.h>
14 #include <cerrno>
16 #include <AutoDeleter.h>
17 #include <DataIO.h>
18 #include <UnicodeChar.h>
20 #include "JsonEventListener.h"
21 #include "JsonMessageWriter.h"
24 // #pragma mark - Public methods
26 namespace BPrivate {
29 static bool
30 b_jsonparse_is_hex(char c)
32 return isdigit(c)
33 || (c > 0x41 && c <= 0x46)
34 || (c > 0x61 && c <= 0x66);
38 static bool
39 b_jsonparse_all_hex(const char* c)
41 for (int i = 0; i < 4; i++) {
42 if (!b_jsonparse_is_hex(c[i]))
43 return false;
46 return true;
50 /*! This class carries state around the parsing process. */
52 class JsonParseContext {
53 public:
54 JsonParseContext(BDataIO* data, BJsonEventListener* listener)
56 fListener(listener),
57 fData(data),
58 fLineNumber(1), // 1 is the first line
59 fPushbackChar(0),
60 fHasPushbackChar(false)
65 BJsonEventListener* Listener() const
67 return fListener;
71 BDataIO* Data() const
73 return fData;
77 int LineNumber() const
79 return fLineNumber;
83 void IncrementLineNumber()
85 fLineNumber++;
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.
93 status_t NextChar(char* buffer)
96 if (fHasPushbackChar) {
97 buffer[0] = fPushbackChar;
98 fHasPushbackChar = false;
99 return B_OK;
102 return Data()->ReadExactly(buffer, 1);
106 void PushbackChar(char c)
108 fPushbackChar = c;
109 fHasPushbackChar = true;
112 private:
113 BJsonEventListener* fListener;
114 BDataIO* fData;
115 uint32 fLineNumber;
116 char fPushbackChar;
117 bool fHasPushbackChar;
121 status_t
122 BJson::Parse(const BString& JSON, BMessage& message)
124 return Parse(JSON.String(), message);
128 status_t
129 BJson::Parse(const char* JSON, BMessage& message)
131 BMemoryIO* input = new BMemoryIO(JSON, strlen(JSON));
132 ObjectDeleter<BMemoryIO> inputDeleter(input);
133 BJsonMessageWriter* writer = new BJsonMessageWriter(message);
134 ObjectDeleter<BJsonMessageWriter> writerDeleter(writer);
136 Parse(input, writer);
137 status_t result = writer->ErrorStatus();
139 return result;
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.
153 void
154 BJson::Parse(BDataIO* data, BJsonEventListener* listener)
156 JsonParseContext context(data, listener);
157 ParseAny(context);
158 listener->Complete();
162 // #pragma mark - Specific parse logic.
165 bool
166 BJson::NextChar(JsonParseContext& jsonParseContext, char* c)
168 status_t result = jsonParseContext.NextChar(c);
170 switch (result) {
171 case B_OK:
172 return true;
174 case B_PARTIAL_READ:
176 jsonParseContext.Listener()->HandleError(B_BAD_DATA,
177 jsonParseContext.LineNumber(), "unexpected end of input");
178 return false;
181 default:
183 jsonParseContext.Listener()->HandleError(result, -1,
184 "io related read error");
185 return false;
191 bool
192 BJson::NextNonWhitespaceChar(JsonParseContext& jsonParseContext, char* c)
194 while (true) {
195 if (!NextChar(jsonParseContext, c))
196 return false;
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;
207 default:
208 return true;
214 bool
215 BJson::ParseAny(JsonParseContext& jsonParseContext)
217 char c;
219 if (!NextNonWhitespaceChar(jsonParseContext, &c))
220 return false;
222 switch (c) {
223 case 'f': // [f]alse
224 return ParseExpectedVerbatimStringAndRaiseEvent(
225 jsonParseContext, "alse", 4, 'f', B_JSON_FALSE);
227 case 't': // [t]rue
228 return ParseExpectedVerbatimStringAndRaiseEvent(
229 jsonParseContext, "rue", 3, 't', B_JSON_TRUE);
231 case 'n': // [n]ull
232 return ParseExpectedVerbatimStringAndRaiseEvent(
233 jsonParseContext, "ull", 3, 'n', B_JSON_NULL);
235 case '"':
236 return ParseString(jsonParseContext, B_JSON_STRING);
238 case '{':
239 return ParseObject(jsonParseContext);
241 case '[':
242 return ParseArray(jsonParseContext);
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);
259 default:
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);
269 jsonParseContext.Listener()->HandleError(B_BAD_DATA,
270 jsonParseContext.LineNumber(), errorMessage.String());
271 return false;
275 return true;
279 /*! This method captures an object name, a separator ':' and then any value. */
281 bool
282 BJson::ParseObjectNameValuePair(JsonParseContext& jsonParseContext)
284 bool didParseName = false;
285 char c;
287 while (true) {
288 if (!NextNonWhitespaceChar(jsonParseContext, &c))
289 return false;
291 switch (c) {
292 case '\"': // name of the object
294 if (!didParseName) {
295 if (!ParseString(jsonParseContext, B_JSON_OBJECT_NAME))
296 return false;
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;
306 break;
309 case ':': // separator
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;
324 default:
326 BString errorMessage;
327 errorMessage.SetToFormat(
328 "unexpected character [%c] when parsing object"
329 " name-value pair",
331 jsonParseContext.Listener()->HandleError(B_BAD_DATA,
332 jsonParseContext.LineNumber(), errorMessage.String());
333 return false;
340 bool
341 BJson::ParseObject(JsonParseContext& jsonParseContext)
343 if (!jsonParseContext.Listener()->Handle(
344 BJsonEvent(B_JSON_OBJECT_START))) {
345 return false;
348 char c;
349 bool firstItem = true;
351 while (true) {
352 if (!NextNonWhitespaceChar(jsonParseContext, &c))
353 return false;
355 switch (c) {
356 case '}': // terminate the object
358 if (!jsonParseContext.Listener()->Handle(
359 BJsonEvent(B_JSON_OBJECT_END))) {
360 return false;
362 return true;
365 case ',': // next value.
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;
375 if (!ParseObjectNameValuePair(jsonParseContext))
376 return false;
377 break;
380 default:
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");
396 return true;
400 bool
401 BJson::ParseArray(JsonParseContext& jsonParseContext)
403 if (!jsonParseContext.Listener()->Handle(
404 BJsonEvent(B_JSON_ARRAY_START))) {
405 return false;
408 char c;
409 bool firstItem = true;
411 while (true) {
412 if (!NextNonWhitespaceChar(jsonParseContext, &c))
413 return false;
415 switch (c) {
416 case ']': // terminate the array
418 if (!jsonParseContext.Listener()->Handle(
419 BJsonEvent(B_JSON_ARRAY_END))) {
420 return false;
422 return true;
425 case ',': // next value.
427 if (firstItem) {
428 jsonParseContext.Listener()->HandleError(B_BAD_DATA,
429 jsonParseContext.LineNumber(), "unexpected"
430 " item separator when parsing start of"
431 " array");
434 if (!ParseAny(jsonParseContext))
435 return false;
436 break;
439 default:
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");
455 return true;
459 bool
460 BJson::ParseEscapeUnicodeSequence(JsonParseContext& jsonParseContext,
461 BString& stringResult)
463 char buffer[5];
464 buffer[4] = 0;
466 if (!NextChar(jsonParseContext, &buffer[0])
467 || !NextChar(jsonParseContext, &buffer[1])
468 || !NextChar(jsonParseContext, &buffer[2])
469 || !NextChar(jsonParseContext, &buffer[3])) {
470 return false;
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;
483 uint intValue;
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;
495 char character[7];
496 char* ptr = character;
497 BUnicodeChar::ToUTF8(intValue, &ptr);
498 int32 sequenceLength = ptr - character;
499 stringResult.Append(character, sequenceLength);
501 return true;
505 bool
506 BJson::ParseStringEscapeSequence(JsonParseContext& jsonParseContext,
507 BString& stringResult)
509 char c;
511 if (!NextChar(jsonParseContext, &c))
512 return false;
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':
541 // unicode escape sequence.
542 if (!ParseEscapeUnicodeSequence(jsonParseContext,
543 stringResult)) {
544 return false;
546 break;
548 default:
550 BString errorMessage;
551 errorMessage.SetToFormat(
552 "unexpected escaped character [%c] in string parsing",
554 jsonParseContext.Listener()->HandleError(B_BAD_DATA,
555 jsonParseContext.LineNumber(), errorMessage.String());
556 return false;
560 return true;
564 bool
565 BJson::ParseString(JsonParseContext& jsonParseContext,
566 json_event_type eventType)
568 char c;
569 BString stringResult;
571 while(true) {
572 if (!NextChar(jsonParseContext, &c))
573 return false;
575 switch (c) {
576 case '"':
578 // terminates the string assembled so far.
579 jsonParseContext.Listener()->Handle(
580 BJsonEvent(eventType, stringResult.String()));
581 return true;
584 case '\\':
586 if (!ParseStringEscapeSequence(jsonParseContext,
587 stringResult)) {
588 return false;
590 break;
593 default:
595 uint8 uc = static_cast<uint8>(c);
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;
607 stringResult.Append(&c, 1);
608 break;
615 bool
616 BJson::ParseExpectedVerbatimStringAndRaiseEvent(
617 JsonParseContext& jsonParseContext, const char* expectedString,
618 size_t expectedStringLength, char leadingChar,
619 json_event_type jsonEventType)
621 if (ParseExpectedVerbatimString(jsonParseContext, expectedString,
622 expectedStringLength, leadingChar)) {
623 if (!jsonParseContext.Listener()->Handle(BJsonEvent(jsonEventType)))
624 return false;
627 return true;
630 /*! This will make sure that the constant string is available at the input. */
632 bool
633 BJson::ParseExpectedVerbatimString(JsonParseContext& jsonParseContext,
634 const char* expectedString, size_t expectedStringLength, char leadingChar)
636 char c;
637 size_t offset = 0;
639 while (offset < expectedStringLength) {
640 if (!NextChar(jsonParseContext, &c))
641 return false;
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;
653 offset++;
656 return true;
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.
666 bool
667 BJson::IsValidNumber(BString& number)
669 int32 offset = 0;
670 int32 len = number.Length();
672 if (offset < len && number[offset] == '-')
673 offset++;
675 if (offset >= len)
676 return false;
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;
688 if (offset < len && number[offset] == '.') {
689 offset++;
691 if (offset >= len)
692 return false;
694 while (offset < len && isdigit(number[offset]))
695 offset++;
698 if (offset < len && (number[offset] == 'E' || number[offset] == 'e')) {
699 offset++;
701 if(offset < len && (number[offset] == '+' || number[offset] == '-'))
702 offset++;
704 if (offset >= len)
705 return false;
707 while (offset < len && isdigit(number[offset]))
708 offset++;
711 return offset == len;
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.
721 bool
722 BJson::ParseNumber(JsonParseContext& jsonParseContext)
724 BString value;
726 while (true) {
727 char c;
728 status_t result = jsonParseContext.NextChar(&c);
730 switch (result) {
731 case B_OK:
733 if (isdigit(c)) {
734 value += c;
735 break;
738 if (NULL != strchr("+-eE.", c)) {
739 value += c;
740 break;
743 jsonParseContext.PushbackChar(c);
744 // intentional fall through
746 case B_PARTIAL_READ:
748 errno = 0;
750 if (!IsValidNumber(value)) {
751 jsonParseContext.Listener()->HandleError(B_BAD_DATA,
752 jsonParseContext.LineNumber(), "malformed number");
753 return false;
756 jsonParseContext.Listener()->Handle(BJsonEvent(B_JSON_NUMBER,
757 value.String()));
759 return true;
761 default:
763 jsonParseContext.Listener()->HandleError(result, -1,
764 "io related read error");
765 return false;
771 } // namespace BPrivate