542 lines
16 KiB
C#
542 lines
16 KiB
C#
/*
|
|
* This is a customized parser, based on "SimpleJson.cs", for JSONNode
|
|
*
|
|
* SimpleJson.cs is open source software and this entire file may be dealt with as described below:
|
|
*/
|
|
//-----------------------------------------------------------------------
|
|
// <copyright file="SimpleJson.cs" company="The Outercurve Foundation">
|
|
// Copyright (c) 2011, The Outercurve Foundation, 2015 Zen Fulcrum LLC
|
|
//
|
|
// Licensed under the MIT License (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
// http://www.opensource.org/licenses/mit-license.php
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
// </copyright>
|
|
// <author>Nathan Totten (ntotten.com), Jim Zimmerman (jimzimmerman.com) and Prabir Shrestha (prabir.me)</author>
|
|
// <website>https://github.com/facebook-csharp-sdk/simple-json</website>
|
|
//-----------------------------------------------------------------------
|
|
|
|
// ReSharper disable InconsistentNaming
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Runtime.Serialization;
|
|
using System.Text;
|
|
|
|
namespace ZenFulcrum.EmbeddedBrowser {
|
|
/// <summary>
|
|
/// This class encodes and decodes JSON strings.
|
|
/// Spec. details, see http://www.json.org/
|
|
///
|
|
/// JSON uses Arrays and Objects. These correspond here to the datatypes JsonArray(IList<object>) and JsonObject(IDictionary<string,object>).
|
|
/// All numbers are parsed to doubles.
|
|
/// </summary>
|
|
internal static class JSONParser {
|
|
private const int TOKEN_NONE = 0;
|
|
private const int TOKEN_CURLY_OPEN = 1;
|
|
private const int TOKEN_CURLY_CLOSE = 2;
|
|
private const int TOKEN_SQUARED_OPEN = 3;
|
|
private const int TOKEN_SQUARED_CLOSE = 4;
|
|
private const int TOKEN_COLON = 5;
|
|
private const int TOKEN_COMMA = 6;
|
|
private const int TOKEN_STRING = 7;
|
|
private const int TOKEN_NUMBER = 8;
|
|
private const int TOKEN_TRUE = 9;
|
|
private const int TOKEN_FALSE = 10;
|
|
private const int TOKEN_NULL = 11;
|
|
private const int BUILDER_CAPACITY = 2000;
|
|
private static readonly char[] EscapeTable;
|
|
private static readonly char[] EscapeCharacters = new char[] { '"', '\\', '\b', '\f', '\n', '\r', '\t' };
|
|
// private static readonly string EscapeCharactersString = new string(EscapeCharacters);
|
|
static JSONParser() {
|
|
EscapeTable = new char[93];
|
|
EscapeTable['"'] = '"';
|
|
EscapeTable['\\'] = '\\';
|
|
EscapeTable['\b'] = 'b';
|
|
EscapeTable['\f'] = 'f';
|
|
EscapeTable['\n'] = 'n';
|
|
EscapeTable['\r'] = 'r';
|
|
EscapeTable['\t'] = 't';
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the string json into a value
|
|
/// </summary>
|
|
/// <param name="json">A JSON string.</param>
|
|
/// <returns>An IList<object>, a IDictionary<string,object>, a double, a string, null, true, or false</returns>
|
|
public static JSONNode Parse(string json) {
|
|
JSONNode obj;
|
|
if (TryDeserializeObject(json, out obj))
|
|
return obj;
|
|
throw new SerializationException("Invalid JSON string");
|
|
}
|
|
/// <summary>
|
|
/// Try parsing the json string into a value.
|
|
/// </summary>
|
|
/// <param name="json">
|
|
/// A JSON string.
|
|
/// </param>
|
|
/// <param name="obj">
|
|
/// The object.
|
|
/// </param>
|
|
/// <returns>
|
|
/// Returns true if successfull otherwise false.
|
|
/// </returns>
|
|
public static bool TryDeserializeObject(string json, out JSONNode obj) {
|
|
bool success = true;
|
|
if (json != null) {
|
|
char[] charArray = json.ToCharArray();
|
|
int index = 0;
|
|
obj = ParseValue(charArray, ref index, ref success);
|
|
} else
|
|
obj = null;
|
|
return success;
|
|
}
|
|
|
|
public static string EscapeToJavascriptString(string jsonString) {
|
|
if (string.IsNullOrEmpty(jsonString))
|
|
return jsonString;
|
|
StringBuilder sb = new StringBuilder();
|
|
char c;
|
|
for (int i = 0; i < jsonString.Length; ) {
|
|
c = jsonString[i++];
|
|
if (c == '\\') {
|
|
int remainingLength = jsonString.Length - i;
|
|
if (remainingLength >= 2) {
|
|
char lookahead = jsonString[i];
|
|
if (lookahead == '\\') {
|
|
sb.Append('\\');
|
|
++i;
|
|
} else if (lookahead == '"') {
|
|
sb.Append("\"");
|
|
++i;
|
|
} else if (lookahead == 't') {
|
|
sb.Append('\t');
|
|
++i;
|
|
} else if (lookahead == 'b') {
|
|
sb.Append('\b');
|
|
++i;
|
|
} else if (lookahead == 'n') {
|
|
sb.Append('\n');
|
|
++i;
|
|
} else if (lookahead == 'r') {
|
|
sb.Append('\r');
|
|
++i;
|
|
}
|
|
}
|
|
} else {
|
|
sb.Append(c);
|
|
}
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
static JSONNode ParseObject(char[] json, ref int index, ref bool success) {
|
|
JSONNode table = new JSONNode(JSONNode.NodeType.Object);
|
|
int token;
|
|
// {
|
|
NextToken(json, ref index);
|
|
bool done = false;
|
|
while (!done) {
|
|
token = LookAhead(json, index);
|
|
if (token == TOKEN_NONE) {
|
|
success = false;
|
|
return null;
|
|
} else if (token == TOKEN_COMMA)
|
|
NextToken(json, ref index);
|
|
else if (token == TOKEN_CURLY_CLOSE) {
|
|
NextToken(json, ref index);
|
|
return table;
|
|
} else {
|
|
// name
|
|
string name = ParseString(json, ref index, ref success);
|
|
if (!success) {
|
|
success = false;
|
|
return null;
|
|
}
|
|
// :
|
|
token = NextToken(json, ref index);
|
|
if (token != TOKEN_COLON) {
|
|
success = false;
|
|
return null;
|
|
}
|
|
// value
|
|
JSONNode value = ParseValue(json, ref index, ref success);
|
|
if (!success) {
|
|
success = false;
|
|
return null;
|
|
}
|
|
table[name] = value;
|
|
}
|
|
}
|
|
return table;
|
|
}
|
|
|
|
static JSONNode ParseArray(char[] json, ref int index, ref bool success) {
|
|
JSONNode array = new JSONNode(JSONNode.NodeType.Array);
|
|
// [
|
|
NextToken(json, ref index);
|
|
bool done = false;
|
|
while (!done) {
|
|
int token = LookAhead(json, index);
|
|
if (token == TOKEN_NONE) {
|
|
success = false;
|
|
return null;
|
|
} else if (token == TOKEN_COMMA)
|
|
NextToken(json, ref index);
|
|
else if (token == TOKEN_SQUARED_CLOSE) {
|
|
NextToken(json, ref index);
|
|
break;
|
|
} else {
|
|
JSONNode value = ParseValue(json, ref index, ref success);
|
|
if (!success)
|
|
return null;
|
|
array.Add(value);
|
|
}
|
|
}
|
|
return array;
|
|
}
|
|
|
|
static JSONNode ParseValue(char[] json, ref int index, ref bool success) {
|
|
switch (LookAhead(json, index)) {
|
|
case TOKEN_STRING:
|
|
return ParseString(json, ref index, ref success);
|
|
case TOKEN_NUMBER:
|
|
return ParseNumber(json, ref index, ref success);
|
|
case TOKEN_CURLY_OPEN:
|
|
return ParseObject(json, ref index, ref success);
|
|
case TOKEN_SQUARED_OPEN:
|
|
return ParseArray(json, ref index, ref success);
|
|
case TOKEN_TRUE:
|
|
NextToken(json, ref index);
|
|
return true;
|
|
case TOKEN_FALSE:
|
|
NextToken(json, ref index);
|
|
return false;
|
|
case TOKEN_NULL:
|
|
NextToken(json, ref index);
|
|
return JSONNode.NullNode;
|
|
case TOKEN_NONE:
|
|
break;
|
|
}
|
|
success = false;
|
|
return JSONNode.InvalidNode;
|
|
}
|
|
|
|
static JSONNode ParseString(char[] json, ref int index, ref bool success) {
|
|
StringBuilder s = new StringBuilder(BUILDER_CAPACITY);
|
|
char c;
|
|
EatWhitespace(json, ref index);
|
|
// "
|
|
c = json[index++];
|
|
bool complete = false;
|
|
while (!complete) {
|
|
if (index == json.Length)
|
|
break;
|
|
c = json[index++];
|
|
if (c == '"') {
|
|
complete = true;
|
|
break;
|
|
} else if (c == '\\') {
|
|
if (index == json.Length)
|
|
break;
|
|
c = json[index++];
|
|
if (c == '"')
|
|
s.Append('"');
|
|
else if (c == '\\')
|
|
s.Append('\\');
|
|
else if (c == '/')
|
|
s.Append('/');
|
|
else if (c == 'b')
|
|
s.Append('\b');
|
|
else if (c == 'f')
|
|
s.Append('\f');
|
|
else if (c == 'n')
|
|
s.Append('\n');
|
|
else if (c == 'r')
|
|
s.Append('\r');
|
|
else if (c == 't')
|
|
s.Append('\t');
|
|
else if (c == 'u') {
|
|
int remainingLength = json.Length - index;
|
|
if (remainingLength >= 4) {
|
|
// parse the 32 bit hex into an integer codepoint
|
|
uint codePoint;
|
|
if (!(success = UInt32.TryParse(new string(json, index, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out codePoint)))
|
|
return "";
|
|
// convert the integer codepoint to a unicode char and add to string
|
|
if (0xD800 <= codePoint && codePoint <= 0xDBFF) // if high surrogate
|
|
{
|
|
index += 4; // skip 4 chars
|
|
remainingLength = json.Length - index;
|
|
if (remainingLength >= 6) {
|
|
uint lowCodePoint;
|
|
if (new string(json, index, 2) == "\\u" && UInt32.TryParse(new string(json, index + 2, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out lowCodePoint)) {
|
|
if (0xDC00 <= lowCodePoint && lowCodePoint <= 0xDFFF) // if low surrogate
|
|
{
|
|
s.Append((char)codePoint);
|
|
s.Append((char)lowCodePoint);
|
|
index += 6; // skip 6 chars
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
success = false; // invalid surrogate pair
|
|
return "";
|
|
}
|
|
s.Append(ConvertFromUtf32((int)codePoint));
|
|
// skip 4 chars
|
|
index += 4;
|
|
} else
|
|
break;
|
|
}
|
|
} else
|
|
s.Append(c);
|
|
}
|
|
if (!complete) {
|
|
success = false;
|
|
return null;
|
|
}
|
|
return s.ToString();
|
|
}
|
|
|
|
private static string ConvertFromUtf32(int utf32) {
|
|
// http://www.java2s.com/Open-Source/CSharp/2.6.4-mono-.net-core/System/System/Char.cs.htm
|
|
if (utf32 < 0 || utf32 > 0x10FFFF)
|
|
throw new ArgumentOutOfRangeException("utf32", "The argument must be from 0 to 0x10FFFF.");
|
|
if (0xD800 <= utf32 && utf32 <= 0xDFFF)
|
|
throw new ArgumentOutOfRangeException("utf32", "The argument must not be in surrogate pair range.");
|
|
if (utf32 < 0x10000)
|
|
return new string((char)utf32, 1);
|
|
utf32 -= 0x10000;
|
|
return new string(new char[] { (char)((utf32 >> 10) + 0xD800), (char)(utf32 % 0x0400 + 0xDC00) });
|
|
}
|
|
|
|
static JSONNode ParseNumber(char[] json, ref int index, ref bool success) {
|
|
EatWhitespace(json, ref index);
|
|
int lastIndex = GetLastIndexOfNumber(json, index);
|
|
int charLength = (lastIndex - index) + 1;
|
|
JSONNode returnNumber;
|
|
string str = new string(json, index, charLength);
|
|
if (str.IndexOf(".", StringComparison.OrdinalIgnoreCase) != -1 || str.IndexOf("e", StringComparison.OrdinalIgnoreCase) != -1) {
|
|
double number;
|
|
success = double.TryParse(new string(json, index, charLength), NumberStyles.Any, CultureInfo.InvariantCulture, out number);
|
|
returnNumber = number;
|
|
} else {
|
|
long number;
|
|
success = long.TryParse(new string(json, index, charLength), NumberStyles.Any, CultureInfo.InvariantCulture, out number);
|
|
returnNumber = number;
|
|
}
|
|
index = lastIndex + 1;
|
|
return returnNumber;
|
|
}
|
|
|
|
static int GetLastIndexOfNumber(char[] json, int index) {
|
|
int lastIndex;
|
|
for (lastIndex = index; lastIndex < json.Length; lastIndex++)
|
|
if ("0123456789+-.eE".IndexOf(json[lastIndex]) == -1) break;
|
|
return lastIndex - 1;
|
|
}
|
|
|
|
static void EatWhitespace(char[] json, ref int index) {
|
|
for (; index < json.Length; index++)
|
|
if (" \t\n\r\b\f".IndexOf(json[index]) == -1) break;
|
|
}
|
|
static int LookAhead(char[] json, int index) {
|
|
int saveIndex = index;
|
|
return NextToken(json, ref saveIndex);
|
|
}
|
|
|
|
static int NextToken(char[] json, ref int index) {
|
|
EatWhitespace(json, ref index);
|
|
if (index == json.Length)
|
|
return TOKEN_NONE;
|
|
char c = json[index];
|
|
index++;
|
|
switch (c) {
|
|
case '{':
|
|
return TOKEN_CURLY_OPEN;
|
|
case '}':
|
|
return TOKEN_CURLY_CLOSE;
|
|
case '[':
|
|
return TOKEN_SQUARED_OPEN;
|
|
case ']':
|
|
return TOKEN_SQUARED_CLOSE;
|
|
case ',':
|
|
return TOKEN_COMMA;
|
|
case '"':
|
|
return TOKEN_STRING;
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
case '-':
|
|
return TOKEN_NUMBER;
|
|
case ':':
|
|
return TOKEN_COLON;
|
|
}
|
|
index--;
|
|
int remainingLength = json.Length - index;
|
|
// false
|
|
if (remainingLength >= 5) {
|
|
if (json[index] == 'f' && json[index + 1] == 'a' && json[index + 2] == 'l' && json[index + 3] == 's' && json[index + 4] == 'e') {
|
|
index += 5;
|
|
return TOKEN_FALSE;
|
|
}
|
|
}
|
|
// true
|
|
if (remainingLength >= 4) {
|
|
if (json[index] == 't' && json[index + 1] == 'r' && json[index + 2] == 'u' && json[index + 3] == 'e') {
|
|
index += 4;
|
|
return TOKEN_TRUE;
|
|
}
|
|
}
|
|
// null
|
|
if (remainingLength >= 4) {
|
|
if (json[index] == 'n' && json[index + 1] == 'u' && json[index + 2] == 'l' && json[index + 3] == 'l') {
|
|
index += 4;
|
|
return TOKEN_NULL;
|
|
}
|
|
}
|
|
return TOKEN_NONE;
|
|
}
|
|
|
|
public static string Serialize(JSONNode node) {
|
|
StringBuilder sb = new StringBuilder();
|
|
var success = SerializeValue(node, sb);
|
|
if (!success) throw new SerializationException("Failed to serialize JSON");
|
|
return sb.ToString();
|
|
}
|
|
|
|
static bool SerializeValue(JSONNode value, StringBuilder builder) {
|
|
bool success = true;
|
|
|
|
// if (value == null) {
|
|
// builder.Append("null");
|
|
// return success;
|
|
// }
|
|
|
|
switch (value.Type) {
|
|
case JSONNode.NodeType.String:
|
|
success = SerializeString(value, builder);
|
|
break;
|
|
case JSONNode.NodeType.Object: {
|
|
Dictionary<String, JSONNode> dict = value;
|
|
success = SerializeObject(dict.Keys, dict.Values, builder);
|
|
break;
|
|
}
|
|
case JSONNode.NodeType.Array:
|
|
success = SerializeArray((List<JSONNode>)value, builder);
|
|
break;
|
|
case JSONNode.NodeType.Number:
|
|
success = SerializeNumber(value, builder);
|
|
break;
|
|
case JSONNode.NodeType.Bool:
|
|
builder.Append(value ? "true" : "false");
|
|
break;
|
|
case JSONNode.NodeType.Null:
|
|
builder.Append("null");
|
|
break;
|
|
case JSONNode.NodeType.Invalid:
|
|
throw new SerializationException("Cannot serialize invalid JSONNode");
|
|
default:
|
|
throw new SerializationException("Unknown JSONNode type");
|
|
}
|
|
return success;
|
|
}
|
|
|
|
static bool SerializeObject(IEnumerable<string> keys, IEnumerable<JSONNode> values, StringBuilder builder) {
|
|
builder.Append("{");
|
|
var ke = keys.GetEnumerator();
|
|
var ve = values.GetEnumerator();
|
|
bool first = true;
|
|
while (ke.MoveNext() && ve.MoveNext()) {
|
|
var key = ke.Current;
|
|
var value = ve.Current;
|
|
if (!first)
|
|
builder.Append(",");
|
|
string stringKey = key;
|
|
if (stringKey != null)
|
|
SerializeString(stringKey, builder);
|
|
else
|
|
if (!SerializeValue(value, builder)) return false;
|
|
builder.Append(":");
|
|
if (!SerializeValue(value, builder))
|
|
return false;
|
|
first = false;
|
|
}
|
|
builder.Append("}");
|
|
return true;
|
|
}
|
|
|
|
static bool SerializeArray(IEnumerable<JSONNode> anArray, StringBuilder builder) {
|
|
builder.Append("[");
|
|
bool first = true;
|
|
foreach (var value in anArray) {
|
|
if (!first)
|
|
builder.Append(",");
|
|
if (!SerializeValue(value, builder))
|
|
return false;
|
|
first = false;
|
|
}
|
|
builder.Append("]");
|
|
return true;
|
|
}
|
|
|
|
static bool SerializeString(string aString, StringBuilder builder) {
|
|
// Happy path if there's nothing to be escaped. IndexOfAny is highly optimized (and unmanaged)
|
|
if (aString.IndexOfAny(EscapeCharacters) == -1) {
|
|
builder.Append('"');
|
|
builder.Append(aString);
|
|
builder.Append('"');
|
|
return true;
|
|
}
|
|
builder.Append('"');
|
|
int safeCharacterCount = 0;
|
|
char[] charArray = aString.ToCharArray();
|
|
for (int i = 0; i < charArray.Length; i++) {
|
|
char c = charArray[i];
|
|
// Non ascii characters are fine, buffer them up and send them to the builder
|
|
// in larger chunks if possible. The escape table is a 1:1 translation table
|
|
// with \0 [default(char)] denoting a safe character.
|
|
if (c >= EscapeTable.Length || EscapeTable[c] == default(char)) {
|
|
safeCharacterCount++;
|
|
} else {
|
|
if (safeCharacterCount > 0) {
|
|
builder.Append(charArray, i - safeCharacterCount, safeCharacterCount);
|
|
safeCharacterCount = 0;
|
|
}
|
|
builder.Append('\\');
|
|
builder.Append(EscapeTable[c]);
|
|
}
|
|
}
|
|
if (safeCharacterCount > 0) {
|
|
builder.Append(charArray, charArray.Length - safeCharacterCount, safeCharacterCount);
|
|
}
|
|
builder.Append('"');
|
|
return true;
|
|
}
|
|
|
|
static bool SerializeNumber(double number, StringBuilder builder) {
|
|
builder.Append(Convert.ToDouble(number, CultureInfo.InvariantCulture).ToString("r", CultureInfo.InvariantCulture));
|
|
return true;
|
|
}
|
|
|
|
}
|
|
|
|
}
|