Initial commit.
[BoarSSL] / Twrch / JSON.cs
1 /*
2 * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining
5 * a copy of this software and associated documentation files (the
6 * "Software"), to deal in the Software without restriction, including
7 * without limitation the rights to use, copy, modify, merge, publish,
8 * distribute, sublicense, and/or sell copies of the Software, and to
9 * permit persons to whom the Software is furnished to do so, subject to
10 * the following conditions:
11 *
12 * The above copyright notice and this permission notice shall be
13 * included in all copies or substantial portions of the Software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
19 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
20 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 * SOFTWARE.
23 */
24
25 using System;
26 using System.Collections.Generic;
27 using System.IO;
28 using System.Text;
29
30 /*
31 * A simple JSON parser.
32 *
33 * A JSON value is returned as:
34 *
35 * - null, if the value is a JSON null;
36 *
37 * - a string, if the value is a JSON string, a JSON number or a
38 * JSON boolean;
39 *
40 * - an IDictionary<string, object>, if the value is a JSON object;
41 *
42 * - an array (object[]), if the value is a JSON array.
43 *
44 * This parser is lenient with numbers, in that it will gleefully
45 * accumulate digits, dots, minus sign, plus sign, lowercase 'e'
46 * and uppercase 'E' characters in any order.
47 */
48
49 public static class JSON {
50
51 /*
52 * Parse a source stream as a JSON object.
53 */
54 public static object Parse(Stream src)
55 {
56 return Parse(new StreamReader(src));
57 }
58
59 /*
60 * Parse a source stream as a JSON object.
61 */
62 public static object Parse(TextReader tr)
63 {
64 int cp = NextNonWS(tr, ' ');
65 object val;
66 cp = ReadValue(tr, cp, out val);
67 while (cp >= 0) {
68 if (!IsWS(cp)) {
69 throw new Exception(
70 "Trailing garbage after JSON value");
71 }
72 cp = tr.Read();
73 }
74 return val;
75 }
76
77 /*
78 * Encode a JSON object onto a stream.
79 */
80 public static void Encode(object obj, Stream dst)
81 {
82 TextWriter tw = new StreamWriter(dst);
83 Encode(obj, tw);
84 tw.Flush();
85 }
86
87 /*
88 * Encode a JSON object onto a stream.
89 */
90 public static void Encode(object obj, TextWriter tw)
91 {
92 EncodeValue(0, obj, tw);
93 tw.WriteLine();
94 }
95
96 /*
97 * Get a value by path. If the value is present, then 'val'
98 * is set to that value (which may be null) and true is returned;
99 * otherwise, 'val' is set to null and false is written.
100 *
101 * An exception is still thrown if one of the upper path elements
102 * does not have the expected type.
103 */
104 public static bool TryGet(object obj, string path, out object val)
105 {
106 int n = path.Length;
107 int p = 0;
108 while (p < n) {
109 int q = path.IndexOf('/', p);
110 if (q < 0) {
111 q = n;
112 }
113 IDictionary<string, object> d =
114 obj as IDictionary<string, object>;
115 if (d == null) {
116 throw new Exception(string.Format(
117 "Path '{0}': not an object",
118 path.Substring(0, p)));
119 }
120 string name = path.Substring(p, q - p);
121 if (!d.ContainsKey(name)) {
122 val = null;
123 return false;
124 }
125 obj = d[name];
126 p = q + 1;
127 }
128 val = obj;
129 return true;
130 }
131
132 /*
133 * Get a value by path.
134 */
135 public static object Get(object obj, string path)
136 {
137 object val;
138 if (!TryGet(obj, path, out val)) {
139 throw new Exception("No such value: " + path);
140 }
141 return val;
142 }
143
144 /*
145 * Try to get a value by path; value (if present) should be a
146 * string.
147 */
148 public static bool TryGetString(object obj, string path, out string val)
149 {
150 object gv;
151 if (!TryGet(obj, path, out gv)) {
152 val = null;
153 return false;
154 }
155 if (!(gv is string)) {
156 throw new Exception("Value at " + path
157 + " is not a string");
158 }
159 val = gv as string;
160 return true;
161 }
162
163 /*
164 * Get a value by path; value should be a string.
165 */
166 public static string GetString(object obj, string path)
167 {
168 string str;
169 if (!TryGetString(obj, path, out str)) {
170 throw new Exception("No such value: " + path);
171 }
172 return str;
173 }
174
175 /*
176 * Try to get a value by path; value should be an array.
177 */
178 public static bool TryGetArray(object obj, string path,
179 out object[] val)
180 {
181 object gv;
182 if (!TryGet(obj, path, out gv)) {
183 val = null;
184 return false;
185 }
186 val = gv as object[];
187 if (val == null) {
188 throw new Exception("Value at " + path
189 + " is not an array");
190 }
191 return true;
192 }
193
194 /*
195 * Get a value by path; value should be an array.
196 */
197 public static object[] GetArray(object obj, string path)
198 {
199 object[] a;
200 if (!TryGetArray(obj, path, out a)) {
201 throw new Exception("No such value: " + path);
202 }
203 return a;
204 }
205
206 /*
207 * Try to get a value by path; if present, value should be an
208 * array, whose elements are all strings. A new, properly typed
209 * array is returned, containing the strings.
210 */
211 public static bool TryGetStringArray(object obj, string path,
212 out string[] a)
213 {
214 object[] g;
215 if (!TryGetArray(obj, path, out g)) {
216 a = null;
217 return false;
218 }
219 string[] r = new string[g.Length];
220 for (int i = 0; i < g.Length; i ++) {
221 string s = g[i] as string;
222 if (s == null) {
223 throw new Exception(string.Format("Element {0}"
224 + " in array {1} is not a string",
225 i, path));
226 }
227 r[i] = s;
228 }
229 a = r;
230 return true;
231 }
232
233 /*
234 * Get a value by path; value should be an array, whose
235 * elements are all strings. A new, properly typed array is
236 * returned, containing the strings.
237 */
238 public static string[] GetStringArray(object obj, string path)
239 {
240 string[] a;
241 if (!TryGetStringArray(obj, path, out a)) {
242 throw new Exception("No such value: " + path);
243 }
244 return a;
245 }
246
247 /*
248 * Try to get a value by path; value should a boolean.
249 */
250 public static bool TryGetBool(object obj, string path, out bool val)
251 {
252 object gv;
253 if (!TryGet(obj, path, out gv)) {
254 val = false;
255 return false;
256 }
257 if (gv is bool) {
258 val = (bool)gv;
259 return true;
260 } else if (gv is string) {
261 switch (gv as string) {
262 case "true": val = true; return true;
263 case "false": val = false; return true;
264 }
265 }
266 throw new Exception("Value at " + path + " is not a boolean");
267 }
268
269 /*
270 * Get a value by path; value should a boolean.
271 */
272 public static bool GetBool(object obj, string path)
273 {
274 bool v;
275 if (!TryGetBool(obj, path, out v)) {
276 throw new Exception("No such value: " + path);
277 }
278 return v;
279 }
280
281 /*
282 * Try to get a value by path; value should an integer.
283 */
284 public static bool TryGetInt32(object obj, string path, out int val)
285 {
286 object gv;
287 if (!TryGet(obj, path, out gv)) {
288 val = 0;
289 return false;
290 }
291 if (gv is int) {
292 val = (int)gv;
293 return true;
294 } else if (gv is uint) {
295 uint x = (uint)gv;
296 if (x <= (uint)Int32.MaxValue) {
297 val = (int)x;
298 return true;
299 }
300 } else if (gv is long) {
301 long x = (long)gv;
302 if (x >= (long)Int32.MinValue
303 && x <= (long)Int32.MaxValue)
304 {
305 val = (int)x;
306 return true;
307 }
308 } else if (gv is ulong) {
309 ulong x = (ulong)gv;
310 if (x <= (ulong)Int32.MaxValue) {
311 val = (int)x;
312 return true;
313 }
314 } else if (gv is string) {
315 int x;
316 if (Int32.TryParse((string)gv, out x)) {
317 val = x;
318 return true;
319 }
320 }
321 throw new Exception("Value at " + path + " is not a boolean");
322 }
323
324 /*
325 * Get a value by path; value should an integer.
326 */
327 public static int GetInt32(object obj, string path)
328 {
329 int v;
330 if (!TryGetInt32(obj, path, out v)) {
331 throw new Exception("No such value: " + path);
332 }
333 return v;
334 }
335
336 /*
337 * Try to get a value by path; value should be an object map.
338 */
339 public static bool TryGetObjectMap(object obj, string path,
340 out IDictionary<string, object> val)
341 {
342 object gv;
343 if (!TryGet(obj, path, out gv)) {
344 val = null;
345 return false;
346 }
347 val = gv as IDictionary<string, object>;
348 if (val == null) {
349 throw new Exception("Value at " + path
350 + " is not an object map");
351 }
352 return true;
353 }
354
355 /*
356 * Get a value by path; value should be an object map.
357 */
358 public static IDictionary<string, object> GetObjectMap(
359 object obj, string path)
360 {
361 IDictionary<string, object> v;
362 if (!TryGetObjectMap(obj, path, out v)) {
363 throw new Exception("No such value: " + path);
364 }
365 return v;
366 }
367
368 static void EncodeValue(int indent, object obj, TextWriter tw)
369 {
370 if (obj == null) {
371 tw.Write("null");
372 return;
373 }
374 if (obj is bool) {
375 tw.Write((bool)obj ? "true" : "false");
376 return;
377 }
378 if (obj is string) {
379 EncodeString((string)obj, tw);
380 return;
381 }
382 if (obj is int || obj is uint || obj is long || obj is ulong) {
383 tw.Write(obj.ToString());
384 return;
385 }
386 if (obj is Array) {
387 tw.Write("[");
388 Array a = (Array)obj;
389 for (int i = 0; i < a.Length; i ++) {
390 if (i != 0) {
391 tw.Write(",");
392 }
393 tw.WriteLine();
394 Indent(indent + 1, tw);
395 EncodeValue(indent + 1, a.GetValue(i), tw);
396 }
397 tw.WriteLine();
398 Indent(indent, tw);
399 tw.Write("]");
400 return;
401 }
402 if (obj is IDictionary<string, object>) {
403 tw.Write("{");
404 IDictionary<string, object> d =
405 (IDictionary<string, object>)obj;
406 bool first = true;
407 foreach (string name in d.Keys) {
408 if (first) {
409 first = false;
410 } else {
411 tw.Write(",");
412 }
413 tw.WriteLine();
414 Indent(indent + 1, tw);
415 EncodeString(name, tw);
416 tw.Write(" : ");
417 EncodeValue(indent + 1, d[name], tw);
418 }
419 tw.WriteLine();
420 Indent(indent, tw);
421 tw.Write("}");
422 return;
423 }
424 throw new Exception("Unknown value type: "
425 + obj.GetType().FullName);
426 }
427
428 static void Indent(int indent, TextWriter tw)
429 {
430 while (indent -- > 0) {
431 tw.Write(" ");
432 }
433 }
434
435 static void EncodeString(string str, TextWriter tw)
436 {
437 tw.Write('\"');
438 foreach (char c in str) {
439 if (c >= 32 && c <= 126) {
440 if (c == '\\' || c == '"') {
441 tw.Write('\\');
442 }
443 tw.Write(c);
444 } else {
445 switch (c) {
446 case '\b':
447 tw.Write("\\b");
448 break;
449 case '\f':
450 tw.Write("\\f");
451 break;
452 case '\n':
453 tw.Write("\\n");
454 break;
455 case '\r':
456 tw.Write("\\r");
457 break;
458 case '\t':
459 tw.Write("\\t");
460 break;
461 default:
462 tw.Write("\\u{0:X4}", (int)c);
463 break;
464 }
465 }
466 }
467 tw.Write('\"');
468 }
469
470 /*
471 * Read a value, that starts with the provided character. The
472 * value is written in 'val'. Returned value is the next
473 * character in the stream, or a synthetic space if the next
474 * character was not read.
475 */
476 static int ReadValue(TextReader tr, int cp, out object val)
477 {
478 switch (cp) {
479 case '"':
480 val = ReadString(tr);
481 return ' ';
482 case '{':
483 val = ReadObject(tr);
484 return ' ';
485 case '[':
486 val = ReadArray(tr);
487 return ' ';
488 case 't':
489 CheckKeyword(tr, "true");
490 val = "true";
491 return ' ';
492 case 'f':
493 CheckKeyword(tr, "false");
494 val = "false";
495 return ' ';
496 case 'n':
497 CheckKeyword(tr, "null");
498 val = null;
499 return ' ';
500 case '-':
501 case '0': case '1': case '2': case '3': case '4':
502 case '5': case '6': case '7': case '8': case '9':
503 StringBuilder sb = new StringBuilder();
504 sb.Append((char)cp);
505 cp = ReadNumber(tr, sb);
506 val = sb.ToString();
507 return cp;
508 default:
509 throw Unexpected(cp);
510 }
511 }
512
513 static string ReadString(TextReader tr)
514 {
515 StringBuilder sb = new StringBuilder();
516 bool lcwb = false;
517 for (;;) {
518 int cp = Next(tr);
519 if (lcwb) {
520 lcwb = false;
521 switch (cp) {
522 case '"': case '\\': case '/':
523 sb.Append((char)cp);
524 break;
525 case 'b':
526 sb.Append('\b');
527 break;
528 case 'f':
529 sb.Append('\f');
530 break;
531 case 'n':
532 sb.Append('\n');
533 break;
534 case 'r':
535 sb.Append('\r');
536 break;
537 case 't':
538 sb.Append('\t');
539 break;
540 case 'u':
541 sb.Append(ReadUnicodeEscape(tr));
542 break;
543 default:
544 throw Unexpected(cp);
545 }
546 } else {
547 if (cp == '\\') {
548 lcwb = true;
549 } else if (cp == '"') {
550 return sb.ToString();
551 } else if (cp <= 0x1F) {
552 throw Unexpected(cp);
553 } else {
554 sb.Append((char)cp);
555 }
556 }
557 }
558 }
559
560 static char ReadUnicodeEscape(TextReader tr)
561 {
562 int acc = 0;
563 for (int i = 0; i < 4; i ++) {
564 int cp = Next(tr);
565 if (cp >= '0' && cp <= '9') {
566 cp -= '0';
567 } else if (cp >= 'A' && cp <= 'F') {
568 cp -= 'A' - 10;
569 } else if (cp >= 'a' && cp <= 'f') {
570 cp -= 'a' - 10;
571 } else {
572 throw Unexpected(cp);
573 }
574 acc = (acc << 4) + cp;
575 }
576 return (char)acc;
577 }
578
579 static IDictionary<string, object> ReadObject(TextReader tr)
580 {
581 IDictionary<string, object> r =
582 new SortedDictionary<string, object>(
583 StringComparer.Ordinal);
584 int cp = NextNonWS(tr, ' ');
585 if (cp == '}') {
586 return r;
587 }
588 for (;;) {
589 if (cp != '"') {
590 throw Unexpected(cp);
591 }
592 string name = ReadString(tr);
593 cp = NextNonWS(tr, ' ');
594 if (cp != ':') {
595 throw Unexpected(cp);
596 }
597 if (r.ContainsKey(name)) {
598 throw new Exception(string.Format(
599 "duplicate key '{0}' in object",
600 name));
601 }
602 object val;
603 cp = NextNonWS(tr, ' ');
604 cp = ReadValue(tr, cp, out val);
605 r[name] = val;
606 cp = NextNonWS(tr, cp);
607 if (cp == '}') {
608 return r;
609 }
610 if (cp != ',') {
611 throw Unexpected(cp);
612 }
613 cp = NextNonWS(tr, ' ');
614 }
615 }
616
617 static object[] ReadArray(TextReader tr)
618 {
619 List<object> r = new List<object>();
620 int cp = NextNonWS(tr, ' ');
621 if (cp == ']') {
622 return r.ToArray();
623 }
624 for (;;) {
625 object val;
626 cp = ReadValue(tr, cp, out val);
627 r.Add(val);
628 cp = NextNonWS(tr, cp);
629 if (cp == ']') {
630 return r.ToArray();
631 }
632 if (cp != ',') {
633 throw Unexpected(cp);
634 }
635 cp = NextNonWS(tr, ' ');
636 }
637 }
638
639 static int ReadNumber(TextReader tr, StringBuilder sb)
640 {
641 int cp;
642 for (;;) {
643 cp = tr.Read();
644 switch (cp) {
645 case '0': case '1': case '2': case '3': case '4':
646 case '5': case '6': case '7': case '8': case '9':
647 case '.': case '-': case '+': case 'e': case 'E':
648 sb.Append((char)cp);
649 break;
650 default:
651 return cp;
652 }
653 }
654 }
655
656 static void CheckKeyword(TextReader tr, string str)
657 {
658 int n = str.Length;
659 for (int i = 1; i < n; i ++) {
660 int cp = Next(tr);
661 if (cp != (int)str[i]) {
662 throw Unexpected(cp);
663 }
664 }
665 }
666
667 static bool IsWS(int cp)
668 {
669 return cp == 9 || cp == 10 || cp == 13 || cp == 32;
670 }
671
672 static int Next(TextReader tr)
673 {
674 int cp = tr.Read();
675 if (cp < 0) {
676 throw new EndOfStreamException();
677 }
678 return cp;
679 }
680
681 static int NextNonWS(TextReader tr, int cp)
682 {
683 while (IsWS(cp)) {
684 cp = Next(tr);
685 }
686 return cp;
687 }
688
689 static Exception Unexpected(int cp)
690 {
691 return new Exception(string.Format(
692 "Unexpected character U+{0:X4}", cp));
693 }
694 }