csharp-sdk-upm/Storage/Source/Internal/Utilities/Json.cs

555 lines
19 KiB
C#
Raw Normal View History

2019-07-19 15:01:34 +08:00
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
{
/// <summary>
/// A simple recursive-descent JSON Parser based on the grammar defined at http://www.json.org
/// and http://tools.ietf.org/html/rfc4627
/// </summary>
public class Json
{
/// <summary>
/// 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).
/// </summary>
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*)(?<frac>\.\d+)?(?<exp>(?:e|E)(?:-|\+)?\d+)?");
private static readonly Regex stringValue = new Regex(startOfString +
"\"(?<content>(?:[^\\\\\"]|(?<escape>\\\\(?:[\\\\\"/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();
}
/// <summary>
/// Parses JSON object syntax (e.g. '{}')
/// </summary>
internal bool AVObject(out object output)
{
output = null;
int initialCurrentIndex = CurrentIndex;
if (!Accept(startObject))
{
return false;
}
var dict = new Dictionary<string, object>();
while (true)
{
object pairValue;
if (!ParseMember(out pairValue))
{
break;
}
var pair = pairValue as Tuple<string, object>;
dict[pair.Item1] = pair.Item2;
if (!Accept(valueSeparator))
{
break;
}
}
if (!Accept(endObject))
{
return false;
}
output = dict;
return true;
}
/// <summary>
/// Parses JSON member syntax (e.g. '"keyname" : null')
/// </summary>
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, object>((string)key, value);
return true;
}
/// <summary>
/// Parses JSON array syntax (e.g. '[]')
/// </summary>
internal bool ParseArray(out object output)
{
output = null;
if (!Accept(startArray))
{
return false;
}
var list = new List<object>();
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;
}
/// <summary>
/// Parses a value (i.e. the right-hand side of an object member assignment or
/// an element in an array)
/// </summary>
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);
}
/// <summary>
/// Parses a JSON string (e.g. '"foo\u1234bar\n"')
/// </summary>
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;
}
/// <summary>
/// Parses a number. Returns a long if the number is an integer or has an exponent,
/// otherwise returns a double.
/// </summary>
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;
}
}
/// <summary>
/// Matches the string to a regex, consuming part of the string and returning the match.
/// </summary>
private bool Accept(Regex matcher, out Match match)
{
match = matcher.Match(Input, CurrentIndex);
if (match.Success)
{
Skip(match.Length);
}
return match.Success;
}
/// <summary>
/// Find the first occurrences of a character, consuming part of the string.
/// </summary>
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;
}
/// <summary>
/// Find the first occurrences of a string, consuming part of the string.
/// </summary>
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;
}
}
/// <summary>
/// Parses a JSON-text as defined in http://tools.ietf.org/html/rfc4627, returning an
/// IDictionary&lt;string, object&gt; or an IList&lt;object&gt; depending on whether
/// the value was an array or dictionary. Nested objects also match these types.
/// </summary>
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.");
}
/// <summary>
/// Encodes a dictionary into a JSON string. Supports values that are
/// IDictionary&lt;string, object&gt;, IList&lt;object&gt;, strings,
/// nulls, and any of the primitive types.
/// </summary>
public static string Encode(IDictionary<string, object> 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();
}
/// <summary>
/// Encodes a list into a JSON string. Supports values that are
/// IDictionary&lt;string, object&gt;, IList&lt;object&gt;, strings,
/// nulls, and any of the primitive types.
/// </summary>
public static string Encode(IList<object> 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<string> 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<IDictionary<string, object>> 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();
}
/// <summary>
/// Encodes an object into a JSON string.
/// </summary>
public static string Encode(object obj)
{
var dict = obj as IDictionary<string, object>;
if (dict != null)
{
return Encode(dict);
}
var list = obj as IList<object>;
if (list != null)
{
return Encode(list);
}
var dicList = obj as IList<IDictionary<string, object>>;
if (dicList != null)
{
return Encode(dicList);
}
var strLists = obj as IList<string>;
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);
}
}
}