You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
531 lines
12 KiB
531 lines
12 KiB
/****************************************************************************** |
|
* Spine Runtimes License Agreement |
|
* Last updated January 1, 2020. Replaces all prior versions. |
|
* |
|
* Copyright (c) 2013-2020, Esoteric Software LLC |
|
* |
|
* Integration of the Spine Runtimes into software or otherwise creating |
|
* derivative works of the Spine Runtimes is permitted under the terms and |
|
* conditions of Section 2 of the Spine Editor License Agreement: |
|
* http://esotericsoftware.com/spine-editor-license |
|
* |
|
* Otherwise, it is permitted to integrate the Spine Runtimes into software |
|
* or otherwise create derivative works of the Spine Runtimes (collectively, |
|
* "Products"), provided that each user of the Products must obtain their own |
|
* Spine Editor license and redistribution of the Products in any form must |
|
* include this license and copyright notice. |
|
* |
|
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY |
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
|
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY |
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, |
|
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND |
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
|
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
*****************************************************************************/ |
|
|
|
using System; |
|
using System.IO; |
|
using System.Text; |
|
using System.Collections; |
|
using System.Globalization; |
|
using System.Collections.Generic; |
|
|
|
namespace Spine { |
|
public static class Json { |
|
public static object Deserialize (TextReader text) { |
|
var parser = new SharpJson.JsonDecoder(); |
|
parser.parseNumbersAsFloat = true; |
|
return parser.Decode(text.ReadToEnd()); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Copyright (c) 2016 Adriano Tinoco d'Oliveira Rezende |
|
* |
|
* Based on the JSON parser by Patrick van Bergen |
|
* http://techblog.procurios.nl/k/news/view/14605/14863/how-do-i-write-my-own-parser-(for-json).html |
|
* |
|
* Changes made: |
|
* |
|
* - Optimized parser speed (deserialize roughly near 3x faster than original) |
|
* - Added support to handle lexer/parser error messages with line numbers |
|
* - Added more fine grained control over type conversions during the parsing |
|
* - Refactory API (Separate Lexer code from Parser code and the Encoder from Decoder) |
|
* |
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software |
|
* and associated documentation files (the "Software"), to deal in the Software without restriction, |
|
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, |
|
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, |
|
* subject to the following conditions: |
|
* The above copyright notice and this permission notice shall be included in all copies or substantial |
|
* portions of the Software. |
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT |
|
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. |
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE |
|
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|
*/ |
|
namespace SharpJson |
|
{ |
|
class Lexer |
|
{ |
|
public enum Token { |
|
None, |
|
Null, |
|
True, |
|
False, |
|
Colon, |
|
Comma, |
|
String, |
|
Number, |
|
CurlyOpen, |
|
CurlyClose, |
|
SquaredOpen, |
|
SquaredClose, |
|
}; |
|
|
|
public bool hasError { |
|
get { |
|
return !success; |
|
} |
|
} |
|
|
|
public int lineNumber { |
|
get; |
|
private set; |
|
} |
|
|
|
public bool parseNumbersAsFloat { |
|
get; |
|
set; |
|
} |
|
|
|
char[] json; |
|
int index = 0; |
|
bool success = true; |
|
char[] stringBuffer = new char[4096]; |
|
|
|
public Lexer(string text) |
|
{ |
|
Reset(); |
|
|
|
json = text.ToCharArray(); |
|
parseNumbersAsFloat = false; |
|
} |
|
|
|
public void Reset() |
|
{ |
|
index = 0; |
|
lineNumber = 1; |
|
success = true; |
|
} |
|
|
|
public string ParseString() |
|
{ |
|
int idx = 0; |
|
StringBuilder builder = null; |
|
|
|
SkipWhiteSpaces(); |
|
|
|
// " |
|
char c = json[index++]; |
|
|
|
bool failed = false; |
|
bool complete = false; |
|
|
|
while (!complete && !failed) { |
|
if (index == json.Length) |
|
break; |
|
|
|
c = json[index++]; |
|
if (c == '"') { |
|
complete = true; |
|
break; |
|
} else if (c == '\\') { |
|
if (index == json.Length) |
|
break; |
|
|
|
c = json[index++]; |
|
|
|
switch (c) { |
|
case '"': |
|
stringBuffer[idx++] = '"'; |
|
break; |
|
case '\\': |
|
stringBuffer[idx++] = '\\'; |
|
break; |
|
case '/': |
|
stringBuffer[idx++] = '/'; |
|
break; |
|
case 'b': |
|
stringBuffer[idx++] = '\b'; |
|
break; |
|
case'f': |
|
stringBuffer[idx++] = '\f'; |
|
break; |
|
case 'n': |
|
stringBuffer[idx++] = '\n'; |
|
break; |
|
case 'r': |
|
stringBuffer[idx++] = '\r'; |
|
break; |
|
case 't': |
|
stringBuffer[idx++] = '\t'; |
|
break; |
|
case 'u': |
|
int remainingLength = json.Length - index; |
|
if (remainingLength >= 4) { |
|
var hex = new string(json, index, 4); |
|
|
|
// XXX: handle UTF |
|
stringBuffer[idx++] = (char) Convert.ToInt32(hex, 16); |
|
|
|
// skip 4 chars |
|
index += 4; |
|
} else { |
|
failed = true; |
|
} |
|
break; |
|
} |
|
} else { |
|
stringBuffer[idx++] = c; |
|
} |
|
|
|
if (idx >= stringBuffer.Length) { |
|
if (builder == null) |
|
builder = new StringBuilder(); |
|
|
|
builder.Append(stringBuffer, 0, idx); |
|
idx = 0; |
|
} |
|
} |
|
|
|
if (!complete) { |
|
success = false; |
|
return null; |
|
} |
|
|
|
if (builder != null) |
|
return builder.ToString (); |
|
else |
|
return new string (stringBuffer, 0, idx); |
|
} |
|
|
|
string GetNumberString() |
|
{ |
|
SkipWhiteSpaces(); |
|
|
|
int lastIndex = GetLastIndexOfNumber(index); |
|
int charLength = (lastIndex - index) + 1; |
|
|
|
var result = new string (json, index, charLength); |
|
|
|
index = lastIndex + 1; |
|
|
|
return result; |
|
} |
|
|
|
public float ParseFloatNumber() |
|
{ |
|
float number; |
|
var str = GetNumberString (); |
|
|
|
if (!float.TryParse (str, NumberStyles.Float, CultureInfo.InvariantCulture, out number)) |
|
return 0; |
|
|
|
return number; |
|
} |
|
|
|
public double ParseDoubleNumber() |
|
{ |
|
double number; |
|
var str = GetNumberString (); |
|
|
|
if (!double.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out number)) |
|
return 0; |
|
|
|
return number; |
|
} |
|
|
|
int GetLastIndexOfNumber(int index) |
|
{ |
|
int lastIndex; |
|
|
|
for (lastIndex = index; lastIndex < json.Length; lastIndex++) { |
|
char ch = json[lastIndex]; |
|
|
|
if ((ch < '0' || ch > '9') && ch != '+' && ch != '-' |
|
&& ch != '.' && ch != 'e' && ch != 'E') |
|
break; |
|
} |
|
|
|
return lastIndex - 1; |
|
} |
|
|
|
void SkipWhiteSpaces() |
|
{ |
|
for (; index < json.Length; index++) { |
|
char ch = json[index]; |
|
|
|
if (ch == '\n') |
|
lineNumber++; |
|
|
|
if (!char.IsWhiteSpace(json[index])) |
|
break; |
|
} |
|
} |
|
|
|
public Token LookAhead() |
|
{ |
|
SkipWhiteSpaces(); |
|
|
|
int savedIndex = index; |
|
return NextToken(json, ref savedIndex); |
|
} |
|
|
|
public Token NextToken() |
|
{ |
|
SkipWhiteSpaces(); |
|
return NextToken(json, ref index); |
|
} |
|
|
|
static Token NextToken(char[] json, ref int index) |
|
{ |
|
if (index == json.Length) |
|
return Token.None; |
|
|
|
char c = json[index++]; |
|
|
|
switch (c) { |
|
case '{': |
|
return Token.CurlyOpen; |
|
case '}': |
|
return Token.CurlyClose; |
|
case '[': |
|
return Token.SquaredOpen; |
|
case ']': |
|
return Token.SquaredClose; |
|
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 class JsonDecoder |
|
{ |
|
public string errorMessage { |
|
get; |
|
private set; |
|
} |
|
|
|
public bool parseNumbersAsFloat { |
|
get; |
|
set; |
|
} |
|
|
|
Lexer lexer; |
|
|
|
public JsonDecoder() |
|
{ |
|
errorMessage = null; |
|
parseNumbersAsFloat = false; |
|
} |
|
|
|
public object Decode(string text) |
|
{ |
|
errorMessage = null; |
|
|
|
lexer = new Lexer(text); |
|
lexer.parseNumbersAsFloat = parseNumbersAsFloat; |
|
|
|
return ParseValue(); |
|
} |
|
|
|
public static object DecodeText(string text) |
|
{ |
|
var builder = new JsonDecoder(); |
|
return builder.Decode(text); |
|
} |
|
|
|
IDictionary<string, object> ParseObject() |
|
{ |
|
var table = new Dictionary<string, object>(); |
|
|
|
// { |
|
lexer.NextToken(); |
|
|
|
while (true) { |
|
var token = lexer.LookAhead(); |
|
|
|
switch (token) { |
|
case Lexer.Token.None: |
|
TriggerError("Invalid token"); |
|
return null; |
|
case Lexer.Token.Comma: |
|
lexer.NextToken(); |
|
break; |
|
case Lexer.Token.CurlyClose: |
|
lexer.NextToken(); |
|
return table; |
|
default: |
|
// name |
|
string name = EvalLexer(lexer.ParseString()); |
|
|
|
if (errorMessage != null) |
|
return null; |
|
|
|
// : |
|
token = lexer.NextToken(); |
|
|
|
if (token != Lexer.Token.Colon) { |
|
TriggerError("Invalid token; expected ':'"); |
|
return null; |
|
} |
|
|
|
// value |
|
object value = ParseValue(); |
|
|
|
if (errorMessage != null) |
|
return null; |
|
|
|
table[name] = value; |
|
break; |
|
} |
|
} |
|
|
|
//return null; // Unreachable code |
|
} |
|
|
|
IList<object> ParseArray() |
|
{ |
|
var array = new List<object>(); |
|
|
|
// [ |
|
lexer.NextToken(); |
|
|
|
while (true) { |
|
var token = lexer.LookAhead(); |
|
|
|
switch (token) { |
|
case Lexer.Token.None: |
|
TriggerError("Invalid token"); |
|
return null; |
|
case Lexer.Token.Comma: |
|
lexer.NextToken(); |
|
break; |
|
case Lexer.Token.SquaredClose: |
|
lexer.NextToken(); |
|
return array; |
|
default: |
|
object value = ParseValue(); |
|
|
|
if (errorMessage != null) |
|
return null; |
|
|
|
array.Add(value); |
|
break; |
|
} |
|
} |
|
|
|
//return null; // Unreachable code |
|
} |
|
|
|
object ParseValue() |
|
{ |
|
switch (lexer.LookAhead()) { |
|
case Lexer.Token.String: |
|
return EvalLexer(lexer.ParseString()); |
|
case Lexer.Token.Number: |
|
if (parseNumbersAsFloat) |
|
return EvalLexer(lexer.ParseFloatNumber()); |
|
else |
|
return EvalLexer(lexer.ParseDoubleNumber()); |
|
case Lexer.Token.CurlyOpen: |
|
return ParseObject(); |
|
case Lexer.Token.SquaredOpen: |
|
return ParseArray(); |
|
case Lexer.Token.True: |
|
lexer.NextToken(); |
|
return true; |
|
case Lexer.Token.False: |
|
lexer.NextToken(); |
|
return false; |
|
case Lexer.Token.Null: |
|
lexer.NextToken(); |
|
return null; |
|
case Lexer.Token.None: |
|
break; |
|
} |
|
|
|
TriggerError("Unable to parse value"); |
|
return null; |
|
} |
|
|
|
void TriggerError(string message) |
|
{ |
|
errorMessage = string.Format("Error: '{0}' at line {1}", |
|
message, lexer.lineNumber); |
|
} |
|
|
|
T EvalLexer<T>(T value) |
|
{ |
|
if (lexer.hasError) |
|
TriggerError("Lexical error ocurred"); |
|
|
|
return value; |
|
} |
|
} |
|
}
|
|
|