using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace LeanCloud.Storage.Internal { /// /// A simple recursive-descent JSON Parser based on the grammar defined at http://www.json.org /// and http://tools.ietf.org/html/rfc4627 /// public class Json { /// /// Place at the start of a regex to force the match to begin wherever the search starts (i.e. /// anchored at the index of the first character of the search, even when that search starts /// in the middle of the string). /// private static readonly string startOfString = "\\G"; private static readonly char startObject = '{'; private static readonly char endObject = '}'; private static readonly char startArray = '['; private static readonly char endArray = ']'; private static readonly char valueSeparator = ','; private static readonly char nameSeparator = ':'; private static readonly char[] falseValue = "false".ToCharArray(); private static readonly char[] trueValue = "true".ToCharArray(); private static readonly char[] nullValue = "null".ToCharArray(); private static readonly Regex numberValue = new Regex(startOfString + @"-?(?:0|[1-9]\d*)(?\.\d+)?(?(?:e|E)(?:-|\+)?\d+)?"); private static readonly Regex stringValue = new Regex(startOfString + "\"(?(?:[^\\\\\"]|(?\\\\(?:[\\\\\"/bfnrt]|u[0-9a-fA-F]{4})))*)\"", RegexOptions.Multiline); private static readonly Regex escapePattern = new Regex("\\\\|\"|[\u0000-\u001F]"); private class JsonStringParser { public string Input { get; private set; } public char[] InputAsArray { get; private set; } private int currentIndex; public int CurrentIndex { get { return currentIndex; } } public void Skip(int skip) { currentIndex += skip; } public JsonStringParser(string input) { Input = input; InputAsArray = input.ToCharArray(); } /// /// Parses JSON object syntax (e.g. '{}') /// internal bool AVObject(out object output) { output = null; int initialCurrentIndex = CurrentIndex; if (!Accept(startObject)) { return false; } var dict = new Dictionary(); while (true) { object pairValue; if (!ParseMember(out pairValue)) { break; } var pair = pairValue as Tuple; dict[pair.Item1] = pair.Item2; if (!Accept(valueSeparator)) { break; } } if (!Accept(endObject)) { return false; } output = dict; return true; } /// /// Parses JSON member syntax (e.g. '"keyname" : null') /// private bool ParseMember(out object output) { output = null; object key; if (!ParseString(out key)) { return false; } if (!Accept(nameSeparator)) { return false; } object value; if (!ParseValue(out value)) { return false; } output = new Tuple((string)key, value); return true; } /// /// Parses JSON array syntax (e.g. '[]') /// internal bool ParseArray(out object output) { output = null; if (!Accept(startArray)) { return false; } var list = new List(); while (true) { object value; if (!ParseValue(out value)) { break; } list.Add(value); if (!Accept(valueSeparator)) { break; } } if (!Accept(endArray)) { return false; } output = list; return true; } /// /// Parses a value (i.e. the right-hand side of an object member assignment or /// an element in an array) /// private bool ParseValue(out object output) { if (Accept(falseValue)) { output = false; return true; } else if (Accept(nullValue)) { output = null; return true; } else if (Accept(trueValue)) { output = true; return true; } return AVObject(out output) || ParseArray(out output) || ParseNumber(out output) || ParseString(out output); } /// /// Parses a JSON string (e.g. '"foo\u1234bar\n"') /// private bool ParseString(out object output) { output = null; Match m; if (!Accept(stringValue, out m)) { return false; } // handle escapes: int offset = 0; var contentCapture = m.Groups["content"]; var builder = new StringBuilder(contentCapture.Value); foreach (Capture escape in m.Groups["escape"].Captures) { int index = (escape.Index - contentCapture.Index) - offset; offset += escape.Length - 1; builder.Remove(index + 1, escape.Length - 1); switch (escape.Value[1]) { case '\"': builder[index] = '\"'; break; case '\\': builder[index] = '\\'; break; case '/': builder[index] = '/'; break; case 'b': builder[index] = '\b'; break; case 'f': builder[index] = '\f'; break; case 'n': builder[index] = '\n'; break; case 'r': builder[index] = '\r'; break; case 't': builder[index] = '\t'; break; case 'u': builder[index] = (char)ushort.Parse(escape.Value.Substring(2), NumberStyles.AllowHexSpecifier); break; default: throw new ArgumentException("Unexpected escape character in string: " + escape.Value); } } output = builder.ToString(); return true; } /// /// Parses a number. Returns a long if the number is an integer or has an exponent, /// otherwise returns a double. /// private bool ParseNumber(out object output) { output = null; Match m; if (!Accept(numberValue, out m)) { return false; } if (m.Groups["frac"].Length > 0 || m.Groups["exp"].Length > 0) { // It's a double. output = double.Parse(m.Value, CultureInfo.InvariantCulture); return true; } else { int temp = 0; if (int.TryParse(m.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out temp)) { output = temp; return true; } output = long.Parse(m.Value, CultureInfo.InvariantCulture); return true; } } /// /// Matches the string to a regex, consuming part of the string and returning the match. /// private bool Accept(Regex matcher, out Match match) { match = matcher.Match(Input, CurrentIndex); if (match.Success) { Skip(match.Length); } return match.Success; } /// /// Find the first occurrences of a character, consuming part of the string. /// private bool Accept(char condition) { int step = 0; int strLen = InputAsArray.Length; int currentStep = currentIndex; char currentChar; // Remove whitespace while (currentStep < strLen && ((currentChar = InputAsArray[currentStep]) == ' ' || currentChar == '\r' || currentChar == '\t' || currentChar == '\n')) { ++step; ++currentStep; } bool match = (currentStep < strLen) && (InputAsArray[currentStep] == condition); if (match) { ++step; ++currentStep; // Remove whitespace while (currentStep < strLen && ((currentChar = InputAsArray[currentStep]) == ' ' || currentChar == '\r' || currentChar == '\t' || currentChar == '\n')) { ++step; ++currentStep; } Skip(step); } return match; } /// /// Find the first occurrences of a string, consuming part of the string. /// private bool Accept(char[] condition) { int step = 0; int strLen = InputAsArray.Length; int currentStep = currentIndex; char currentChar; // Remove whitespace while (currentStep < strLen && ((currentChar = InputAsArray[currentStep]) == ' ' || currentChar == '\r' || currentChar == '\t' || currentChar == '\n')) { ++step; ++currentStep; } bool strMatch = true; for (int i = 0; currentStep < strLen && i < condition.Length; ++i, ++currentStep) { if (InputAsArray[currentStep] != condition[i]) { strMatch = false; break; } } bool match = (currentStep < strLen) && strMatch; if (match) { Skip(step + condition.Length); } return match; } } /// /// Parses a JSON-text as defined in http://tools.ietf.org/html/rfc4627, returning an /// IDictionary<string, object> or an IList<object> depending on whether /// the value was an array or dictionary. Nested objects also match these types. /// public static object Parse(string input) { object output; input = input.Trim(); JsonStringParser parser = new JsonStringParser(input); if ((parser.AVObject(out output) || parser.ParseArray(out output)) && parser.CurrentIndex == input.Length) { return output; } throw new ArgumentException("Input JSON was invalid."); } /// /// Encodes a dictionary into a JSON string. Supports values that are /// IDictionary<string, object>, IList<object>, strings, /// nulls, and any of the primitive types. /// public static string Encode(IDictionary dict) { if (dict == null) { throw new ArgumentNullException(); } if (dict.Count == 0) { return "{}"; } var builder = new StringBuilder("{"); foreach (var pair in dict) { builder.Append(Encode(pair.Key)); builder.Append(":"); builder.Append(Encode(pair.Value)); builder.Append(","); } builder[builder.Length - 1] = '}'; return builder.ToString(); } /// /// Encodes a list into a JSON string. Supports values that are /// IDictionary<string, object>, IList<object>, strings, /// nulls, and any of the primitive types. /// public static string Encode(IList list) { if (list == null) { throw new ArgumentNullException(); } if (list.Count == 0) { return "[]"; } var builder = new StringBuilder("["); foreach (var item in list) { builder.Append(Encode(item)); builder.Append(","); } builder[builder.Length - 1] = ']'; return builder.ToString(); } public static string Encode(IList strList) { if (strList == null) { throw new ArgumentNullException(); } if (strList.Count == 0) { return "[]"; } StringBuilder stringBuilder = new StringBuilder("["); foreach (object obj in strList) { stringBuilder.Append(Json.Encode(obj)); stringBuilder.Append(","); } stringBuilder[stringBuilder.Length - 1] = ']'; return stringBuilder.ToString(); } public static string Encode(IList> dicList) { if (dicList == null) { throw new ArgumentNullException(); } if (dicList.Count == 0) { return "[]"; } StringBuilder stringBuilder = new StringBuilder("["); foreach (object obj in dicList) { stringBuilder.Append(Json.Encode(obj)); stringBuilder.Append(","); } stringBuilder[stringBuilder.Length - 1] = ']'; return stringBuilder.ToString(); } /// /// Encodes an object into a JSON string. /// public static string Encode(object obj) { var dict = obj as IDictionary; if (dict != null) { return Encode(dict); } var list = obj as IList; if (list != null) { return Encode(list); } var dicList = obj as IList>; if (dicList != null) { return Encode(dicList); } var strLists = obj as IList; if (strLists != null) { return Encode(strLists); } var str = obj as string; if (str != null) { str = escapePattern.Replace(str, m => { switch (m.Value[0]) { case '\\': return "\\\\"; case '\"': return "\\\""; case '\b': return "\\b"; case '\f': return "\\f"; case '\n': return "\\n"; case '\r': return "\\r"; case '\t': return "\\t"; default: return "\\u" + ((ushort)m.Value[0]).ToString("x4"); } }); return "\"" + str + "\""; } if (obj == null) { return "null"; } if (obj is bool) { if ((bool)obj) { return "true"; } else { return "false"; } } if (!obj.GetType().GetTypeInfo().IsPrimitive) { throw new ArgumentException("Unable to encode objects of type " + obj.GetType()); } return Convert.ToString(obj, CultureInfo.InvariantCulture); } } }