2 * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
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:
12 * The above copyright notice and this permission notice shall be
13 * included in all copies or substantial portions of the Software.
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
26 using System.Collections.Generic;
31 * A simple JSON parser.
33 * A JSON value is returned as:
35 * - null, if the value is a JSON null;
37 * - a string, if the value is a JSON string, a JSON number or a
40 * - an IDictionary<string, object>, if the value is a JSON object;
42 * - an array (object[]), if the value is a JSON array.
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.
49 public static class JSON {
52 * Parse a source stream as a JSON object.
54 public static object Parse(Stream src)
56 return Parse(new StreamReader(src));
60 * Parse a source stream as a JSON object.
62 public static object Parse(TextReader tr)
64 int cp = NextNonWS(tr, ' ');
66 cp = ReadValue(tr, cp, out val);
70 "Trailing garbage after JSON value");
78 * Encode a JSON object onto a stream.
80 public static void Encode(object obj, Stream dst)
82 TextWriter tw = new StreamWriter(dst);
88 * Encode a JSON object onto a stream.
90 public static void Encode(object obj, TextWriter tw)
92 EncodeValue(0, obj, tw);
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.
101 * An exception is still thrown if one of the upper path elements
102 * does not have the expected type.
104 public static bool TryGet(object obj, string path, out object val)
109 int q = path.IndexOf('/', p);
113 IDictionary<string, object> d =
114 obj as IDictionary<string, object>;
116 throw new Exception(string.Format(
117 "Path '{0}': not an object",
118 path.Substring(0, p)));
120 string name = path.Substring(p, q - p);
121 if (!d.ContainsKey(name)) {
133 * Get a value by path.
135 public static object Get(object obj, string path)
138 if (!TryGet(obj, path, out val)) {
139 throw new Exception("No such value: " + path);
145 * Try to get a value by path; value (if present) should be a
148 public static bool TryGetString(object obj, string path, out string val)
151 if (!TryGet(obj, path, out gv)) {
155 if (!(gv is string)) {
156 throw new Exception("Value at " + path
157 + " is not a string");
164 * Get a value by path; value should be a string.
166 public static string GetString(object obj, string path)
169 if (!TryGetString(obj, path, out str)) {
170 throw new Exception("No such value: " + path);
176 * Try to get a value by path; value should be an array.
178 public static bool TryGetArray(object obj, string path,
182 if (!TryGet(obj, path, out gv)) {
186 val = gv as object[];
188 throw new Exception("Value at " + path
189 + " is not an array");
195 * Get a value by path; value should be an array.
197 public static object[] GetArray(object obj, string path)
200 if (!TryGetArray(obj, path, out a)) {
201 throw new Exception("No such value: " + path);
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.
211 public static bool TryGetStringArray(object obj, string path,
215 if (!TryGetArray(obj, path, out g)) {
219 string[] r = new string[g.Length];
220 for (int i = 0; i < g.Length; i ++) {
221 string s = g[i] as string;
223 throw new Exception(string.Format("Element {0}"
224 + " in array {1} is not a string",
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.
238 public static string[] GetStringArray(object obj, string path)
241 if (!TryGetStringArray(obj, path, out a)) {
242 throw new Exception("No such value: " + path);
248 * Try to get a value by path; value should a boolean.
250 public static bool TryGetBool(object obj, string path, out bool val)
253 if (!TryGet(obj, path, out gv)) {
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;
266 throw new Exception("Value at " + path + " is not a boolean");
270 * Get a value by path; value should a boolean.
272 public static bool GetBool(object obj, string path)
275 if (!TryGetBool(obj, path, out v)) {
276 throw new Exception("No such value: " + path);
282 * Try to get a value by path; value should an integer.
284 public static bool TryGetInt32(object obj, string path, out int val)
287 if (!TryGet(obj, path, out gv)) {
294 } else if (gv is uint) {
296 if (x <= (uint)Int32.MaxValue) {
300 } else if (gv is long) {
302 if (x >= (long)Int32.MinValue
303 && x <= (long)Int32.MaxValue)
308 } else if (gv is ulong) {
310 if (x <= (ulong)Int32.MaxValue) {
314 } else if (gv is string) {
316 if (Int32.TryParse((string)gv, out x)) {
321 throw new Exception("Value at " + path + " is not a boolean");
325 * Get a value by path; value should an integer.
327 public static int GetInt32(object obj, string path)
330 if (!TryGetInt32(obj, path, out v)) {
331 throw new Exception("No such value: " + path);
337 * Try to get a value by path; value should be an object map.
339 public static bool TryGetObjectMap(object obj, string path,
340 out IDictionary<string, object> val)
343 if (!TryGet(obj, path, out gv)) {
347 val = gv as IDictionary<string, object>;
349 throw new Exception("Value at " + path
350 + " is not an object map");
356 * Get a value by path; value should be an object map.
358 public static IDictionary<string, object> GetObjectMap(
359 object obj, string path)
361 IDictionary<string, object> v;
362 if (!TryGetObjectMap(obj, path, out v)) {
363 throw new Exception("No such value: " + path);
368 static void EncodeValue(int indent, object obj, TextWriter tw)
375 tw.Write((bool)obj ? "true" : "false");
379 EncodeString((string)obj, tw);
382 if (obj is int || obj is uint || obj is long || obj is ulong) {
383 tw.Write(obj.ToString());
388 Array a = (Array)obj;
389 for (int i = 0; i < a.Length; i ++) {
394 Indent(indent + 1, tw);
395 EncodeValue(indent + 1, a.GetValue(i), tw);
402 if (obj is IDictionary<string, object>) {
404 IDictionary<string, object> d =
405 (IDictionary<string, object>)obj;
407 foreach (string name in d.Keys) {
414 Indent(indent + 1, tw);
415 EncodeString(name, tw);
417 EncodeValue(indent + 1, d[name], tw);
424 throw new Exception("Unknown value type: "
425 + obj.GetType().FullName);
428 static void Indent(int indent, TextWriter tw)
430 while (indent -- > 0) {
435 static void EncodeString(string str, TextWriter tw)
438 foreach (char c in str) {
439 if (c >= 32 && c <= 126) {
440 if (c == '\\' || c == '"') {
462 tw.Write("\\u{0:X4}", (int)c);
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.
476 static int ReadValue(TextReader tr, int cp, out object val)
480 val = ReadString(tr);
483 val = ReadObject(tr);
489 CheckKeyword(tr, "true");
493 CheckKeyword(tr, "false");
497 CheckKeyword(tr, "null");
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();
505 cp = ReadNumber(tr, sb);
509 throw Unexpected(cp);
513 static string ReadString(TextReader tr)
515 StringBuilder sb = new StringBuilder();
522 case '"': case '\\': case '/':
541 sb.Append(ReadUnicodeEscape(tr));
544 throw Unexpected(cp);
549 } else if (cp == '"') {
550 return sb.ToString();
551 } else if (cp <= 0x1F) {
552 throw Unexpected(cp);
560 static char ReadUnicodeEscape(TextReader tr)
563 for (int i = 0; i < 4; i ++) {
565 if (cp >= '0' && cp <= '9') {
567 } else if (cp >= 'A' && cp <= 'F') {
569 } else if (cp >= 'a' && cp <= 'f') {
572 throw Unexpected(cp);
574 acc = (acc << 4) + cp;
579 static IDictionary<string, object> ReadObject(TextReader tr)
581 IDictionary<string, object> r =
582 new SortedDictionary<string, object>(
583 StringComparer.Ordinal);
584 int cp = NextNonWS(tr, ' ');
590 throw Unexpected(cp);
592 string name = ReadString(tr);
593 cp = NextNonWS(tr, ' ');
595 throw Unexpected(cp);
597 if (r.ContainsKey(name)) {
598 throw new Exception(string.Format(
599 "duplicate key '{0}' in object",
603 cp = NextNonWS(tr, ' ');
604 cp = ReadValue(tr, cp, out val);
606 cp = NextNonWS(tr, cp);
611 throw Unexpected(cp);
613 cp = NextNonWS(tr, ' ');
617 static object[] ReadArray(TextReader tr)
619 List<object> r = new List<object>();
620 int cp = NextNonWS(tr, ' ');
626 cp = ReadValue(tr, cp, out val);
628 cp = NextNonWS(tr, cp);
633 throw Unexpected(cp);
635 cp = NextNonWS(tr, ' ');
639 static int ReadNumber(TextReader tr, StringBuilder sb)
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':
656 static void CheckKeyword(TextReader tr, string str)
659 for (int i = 1; i < n; i ++) {
661 if (cp != (int)str[i]) {
662 throw Unexpected(cp);
667 static bool IsWS(int cp)
669 return cp == 9 || cp == 10 || cp == 13 || cp == 32;
672 static int Next(TextReader tr)
676 throw new EndOfStreamException();
681 static int NextNonWS(TextReader tr, int cp)
689 static Exception Unexpected(int cp)
691 return new Exception(string.Format(
692 "Unexpected character U+{0:X4}", cp));