772 lines
16 KiB
C#
772 lines
16 KiB
C#
/*
|
|
* Simple recursive descending JSON parser and
|
|
* JSON string builder.
|
|
*
|
|
* Jonas Drewsen - (C) Unity3d.com - 2010-2012
|
|
*
|
|
* JSONParser parser = new JSONParser(" { \"hello\" : 42.3 } ");
|
|
* JSONValue value = parser.Parse();
|
|
*
|
|
* bool is_it_float = value.isFloat();
|
|
* float the_float = value.asFloat();
|
|
* string the_string = value.Get("sub.structure.access").asString();
|
|
*
|
|
*/
|
|
|
|
using System.Collections.Generic;
|
|
using System;
|
|
|
|
namespace AssetStoreTools.Utility.Json
|
|
{
|
|
|
|
/*
|
|
* JSON value structure
|
|
*
|
|
* Example:
|
|
* JSONValue v = JSONValue.NewDict();
|
|
* v["hello"] = JSONValue.NewString("world");
|
|
* asset(v["hello"].AsString() == "world");
|
|
*
|
|
*/
|
|
internal struct JsonValue
|
|
{
|
|
public JsonValue(object o)
|
|
{
|
|
data = o;
|
|
}
|
|
public static implicit operator JsonValue(string s)
|
|
{
|
|
return new JsonValue(s);
|
|
}
|
|
|
|
public static implicit operator string(JsonValue s)
|
|
{
|
|
return s.AsString();
|
|
}
|
|
|
|
public static implicit operator JsonValue(float s)
|
|
{
|
|
return new JsonValue(s);
|
|
}
|
|
|
|
public static implicit operator float(JsonValue s)
|
|
{
|
|
return s.AsFloat();
|
|
}
|
|
|
|
public static implicit operator JsonValue(bool s)
|
|
{
|
|
return new JsonValue(s);
|
|
}
|
|
|
|
public static implicit operator bool(JsonValue s)
|
|
{
|
|
return s.AsBool();
|
|
}
|
|
|
|
public static implicit operator JsonValue(int s)
|
|
{
|
|
return new JsonValue((float)s);
|
|
}
|
|
|
|
public static implicit operator int(JsonValue s)
|
|
{
|
|
return (int)s.AsFloat();
|
|
}
|
|
|
|
public static implicit operator JsonValue(List<JsonValue> s)
|
|
{
|
|
return new JsonValue(s);
|
|
}
|
|
|
|
public static implicit operator List<JsonValue>(JsonValue s)
|
|
{
|
|
return s.AsList();
|
|
}
|
|
|
|
public static implicit operator Dictionary<string, JsonValue>(JsonValue s)
|
|
{
|
|
return s.AsDict();
|
|
}
|
|
|
|
public bool IsString() { return data is string; }
|
|
public bool IsFloat() { return data is float; }
|
|
public bool IsList() { return data is List<JsonValue>; }
|
|
public bool IsDict() { return data is Dictionary<string, JsonValue>; }
|
|
public bool IsBool() { return data is bool; }
|
|
public bool IsNull() { return data == null; }
|
|
|
|
public string AsString(bool nothrow = false)
|
|
{
|
|
if (data is string)
|
|
return (string)data;
|
|
if (!nothrow)
|
|
throw new JSONTypeException("Tried to read non-string json value as string");
|
|
return "";
|
|
}
|
|
public float AsFloat(bool nothrow = false)
|
|
{
|
|
if (data is float)
|
|
return (float)data;
|
|
if (!nothrow)
|
|
throw new JSONTypeException("Tried to read non-float json value as float");
|
|
return 0.0f;
|
|
}
|
|
public bool AsBool(bool nothrow = false)
|
|
{
|
|
if (data is bool)
|
|
return (bool)data;
|
|
if (!nothrow)
|
|
throw new JSONTypeException("Tried to read non-bool json value as bool");
|
|
return false;
|
|
}
|
|
public List<JsonValue> AsList(bool nothrow = false)
|
|
{
|
|
if (data is List<JsonValue>)
|
|
return (List<JsonValue>)data;
|
|
if (!nothrow)
|
|
throw new JSONTypeException("Tried to read " + data.GetType().Name + " json value as list");
|
|
return null;
|
|
}
|
|
public Dictionary<string, JsonValue> AsDict(bool nothrow = false)
|
|
{
|
|
if (data is Dictionary<string, JsonValue>)
|
|
return (Dictionary<string, JsonValue>)data;
|
|
if (!nothrow)
|
|
throw new JSONTypeException("Tried to read non-dictionary json value as dictionary");
|
|
return null;
|
|
}
|
|
|
|
public static JsonValue NewString(string val)
|
|
{
|
|
return new JsonValue(val);
|
|
}
|
|
|
|
public static JsonValue NewFloat(float val)
|
|
{
|
|
return new JsonValue(val);
|
|
}
|
|
|
|
public static JsonValue NewDict()
|
|
{
|
|
return new JsonValue(new Dictionary<string, JsonValue>());
|
|
}
|
|
|
|
public static JsonValue NewList()
|
|
{
|
|
return new JsonValue(new List<JsonValue>());
|
|
}
|
|
|
|
public static JsonValue NewBool(bool val)
|
|
{
|
|
return new JsonValue(val);
|
|
}
|
|
|
|
public static JsonValue NewNull()
|
|
{
|
|
return new JsonValue(null);
|
|
}
|
|
|
|
public JsonValue InitList()
|
|
{
|
|
data = new List<JsonValue>();
|
|
return this;
|
|
}
|
|
|
|
public JsonValue InitDict()
|
|
{
|
|
data = new Dictionary<string, JsonValue>();
|
|
return this;
|
|
}
|
|
|
|
public JsonValue this[string index]
|
|
{
|
|
get
|
|
{
|
|
Dictionary<string, JsonValue> dict = AsDict();
|
|
return dict[index];
|
|
}
|
|
set
|
|
{
|
|
if (data == null)
|
|
data = new Dictionary<string, JsonValue>();
|
|
Dictionary<string, JsonValue> dict = AsDict();
|
|
dict[index] = value;
|
|
}
|
|
}
|
|
|
|
public bool ContainsKey(string index)
|
|
{
|
|
if (!IsDict())
|
|
return false;
|
|
return AsDict().ContainsKey(index);
|
|
}
|
|
|
|
// Get the specified field in a dict or null json value if
|
|
// no such field exists. The key can point to a nested structure
|
|
// e.g. key1.key2 in { key1 : { key2 : 32 } }
|
|
public JsonValue Get(string key, out bool found)
|
|
{
|
|
found = false;
|
|
if (!IsDict())
|
|
return new JsonValue(null);
|
|
JsonValue value = this;
|
|
foreach (string part in key.Split('.'))
|
|
{
|
|
|
|
if (!value.ContainsKey(part))
|
|
return new JsonValue(null);
|
|
value = value[part];
|
|
}
|
|
found = true;
|
|
return value;
|
|
}
|
|
|
|
public JsonValue Get(string key)
|
|
{
|
|
bool found;
|
|
return Get(key, out found);
|
|
}
|
|
|
|
public bool Copy(string key, ref string dest)
|
|
{
|
|
return Copy(key, ref dest, true);
|
|
}
|
|
|
|
public bool Copy(string key, ref string dest, bool allowCopyNull)
|
|
{
|
|
bool found;
|
|
JsonValue jv = Get(key, out found);
|
|
if (found && (!jv.IsNull() || allowCopyNull))
|
|
dest = jv.IsNull() ? null : jv.AsString();
|
|
return found;
|
|
}
|
|
|
|
public bool Copy(string key, ref bool dest)
|
|
{
|
|
bool found;
|
|
JsonValue jv = Get(key, out found);
|
|
if (found && !jv.IsNull())
|
|
dest = jv.AsBool();
|
|
return found;
|
|
}
|
|
|
|
public bool Copy(string key, ref int dest)
|
|
{
|
|
bool found;
|
|
JsonValue jv = Get(key, out found);
|
|
if (found && !jv.IsNull())
|
|
dest = (int)jv.AsFloat();
|
|
return found;
|
|
}
|
|
|
|
// Convenience dict value setting
|
|
public void Set(string key, string value)
|
|
{
|
|
Set(key, value, true);
|
|
}
|
|
public void Set(string key, string value, bool allowNull)
|
|
{
|
|
if (value == null)
|
|
{
|
|
if (!allowNull)
|
|
return;
|
|
this[key] = NewNull();
|
|
return;
|
|
}
|
|
this[key] = NewString(value);
|
|
}
|
|
|
|
// Convenience dict value setting
|
|
public void Set(string key, float value)
|
|
{
|
|
this[key] = NewFloat(value);
|
|
}
|
|
|
|
// Convenience dict value setting
|
|
public void Set(string key, bool value)
|
|
{
|
|
this[key] = NewBool(value);
|
|
}
|
|
|
|
// Convenience list value add
|
|
public void Add(string value)
|
|
{
|
|
List<JsonValue> list = AsList();
|
|
if (value == null)
|
|
{
|
|
list.Add(NewNull());
|
|
return;
|
|
}
|
|
list.Add(NewString(value));
|
|
}
|
|
|
|
// Convenience list value add
|
|
public void Add(float value)
|
|
{
|
|
List<JsonValue> list = AsList();
|
|
list.Add(NewFloat(value));
|
|
}
|
|
|
|
// Convenience list value add
|
|
public void Add(bool value)
|
|
{
|
|
List<JsonValue> list = AsList();
|
|
list.Add(NewBool(value));
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return ToString(null, "");
|
|
}
|
|
/*
|
|
* Serialize a JSON value to string.
|
|
* This will recurse down through dicts and list type JSONValues.
|
|
*/
|
|
public string ToString(string curIndent, string indent)
|
|
{
|
|
bool indenting = curIndent != null;
|
|
|
|
if (IsString())
|
|
{
|
|
return "\"" + EncodeString(AsString()) + "\"";
|
|
}
|
|
else if (IsFloat())
|
|
{
|
|
return AsFloat().ToString();
|
|
}
|
|
else if (IsList())
|
|
{
|
|
string res = "[";
|
|
string delim = "";
|
|
foreach (JsonValue i in AsList())
|
|
{
|
|
res += delim + i.ToString();
|
|
delim = ", ";
|
|
}
|
|
return res + "]";
|
|
}
|
|
else if (IsDict())
|
|
{
|
|
string res = "{" + (indenting ? "\n" : "");
|
|
string delim = "";
|
|
foreach (KeyValuePair<string, JsonValue> kv in AsDict())
|
|
{
|
|
res += delim + curIndent + indent + '"' + EncodeString(kv.Key) + "\" : " + kv.Value.ToString(curIndent + indent, indent);
|
|
delim = ", " + (indenting ? "\n" : "");
|
|
}
|
|
return res + (indenting ? "\n" + curIndent : "") + "}";
|
|
}
|
|
else if (IsBool())
|
|
{
|
|
return AsBool() ? "true" : "false";
|
|
}
|
|
else if (IsNull())
|
|
{
|
|
return "null";
|
|
}
|
|
else
|
|
{
|
|
throw new JSONTypeException("Cannot serialize json value of unknown type");
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Encode a string into a json string
|
|
private static string EncodeString(string str)
|
|
{
|
|
str = str.Replace("\\", "\\\\");
|
|
str = str.Replace("\"", "\\\"");
|
|
str = str.Replace("/", "\\/");
|
|
str = str.Replace("\b", "\\b");
|
|
str = str.Replace("\f", "\\f");
|
|
str = str.Replace("\n", "\\n");
|
|
str = str.Replace("\r", "\\r");
|
|
str = str.Replace("\t", "\\t");
|
|
// We do not use \uXXXX specifier but direct unicode in the string.
|
|
return str;
|
|
}
|
|
|
|
object data;
|
|
}
|
|
|
|
internal class JSONParseException : Exception
|
|
{
|
|
public JSONParseException(string msg) : base(msg)
|
|
{
|
|
}
|
|
}
|
|
|
|
internal class JSONTypeException : Exception
|
|
{
|
|
public JSONTypeException(string msg) : base(msg)
|
|
{
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Top down recursive JSON parser
|
|
*
|
|
* Example:
|
|
* string json = "{ \"hello\" : \"world\", \"age\" : 100000, "sister" : null }";
|
|
* JSONValue val = JSONParser.SimpleParse(json);
|
|
* asset( val["hello"].AsString() == "world" );
|
|
*
|
|
*/
|
|
internal class JSONParser
|
|
{
|
|
private string json;
|
|
private int line;
|
|
private int linechar;
|
|
private int len;
|
|
private int idx;
|
|
private int pctParsed;
|
|
private char cur;
|
|
|
|
public static JsonValue SimpleParse(string jsondata)
|
|
{
|
|
var parser = new JSONParser(jsondata);
|
|
try
|
|
{
|
|
return parser.Parse();
|
|
}
|
|
catch (JSONParseException ex)
|
|
{
|
|
Console.WriteLine(ex.Message);
|
|
//DebugUtils.LogError(ex.Message);
|
|
}
|
|
return new JsonValue(null);
|
|
}
|
|
|
|
public static bool AssetStoreResponseParse(string responseJson, out ASError error, out JsonValue jval)
|
|
{
|
|
jval = new JsonValue();
|
|
error = null;
|
|
|
|
try
|
|
{
|
|
JSONParser parser = new JSONParser(responseJson);
|
|
jval = parser.Parse();
|
|
}
|
|
catch (JSONParseException)
|
|
{
|
|
error = ASError.GetGenericError(new Exception("Error parsing reply from AssetStore"));
|
|
return false;
|
|
}
|
|
|
|
// Some json responses return an error field on error
|
|
if (jval.ContainsKey("error"))
|
|
{
|
|
// Server side error message
|
|
// Do not write to console since this is an error that
|
|
// is "expected" ie. can be handled by the gui.
|
|
error = ASError.GetGenericError(new Exception(jval["error"].AsString(true)));
|
|
}
|
|
// Some json responses return status+message fields instead of an error field. Go figure.
|
|
else if (jval.ContainsKey("status") && jval["status"].AsString(true) != "ok")
|
|
{
|
|
error = ASError.GetGenericError(new Exception(jval["message"].AsString(true)));
|
|
}
|
|
return error == null;
|
|
}
|
|
|
|
/*
|
|
* Setup a parse to be ready for parsing the given string
|
|
*/
|
|
public JSONParser(string jsondata)
|
|
{
|
|
// TODO: fix that parser needs trailing spaces;
|
|
json = jsondata + " ";
|
|
line = 1;
|
|
linechar = 1;
|
|
len = json.Length;
|
|
idx = 0;
|
|
pctParsed = 0;
|
|
}
|
|
|
|
/*
|
|
* Parse the entire json data string into a JSONValue structure hierarchy
|
|
*/
|
|
public JsonValue Parse()
|
|
{
|
|
cur = json[idx];
|
|
return ParseValue();
|
|
}
|
|
|
|
private char Next()
|
|
{
|
|
if (cur == '\n')
|
|
{
|
|
line++;
|
|
linechar = 0;
|
|
}
|
|
idx++;
|
|
if (idx >= len)
|
|
throw new JSONParseException("End of json while parsing at " + PosMsg());
|
|
|
|
linechar++;
|
|
|
|
int newPct = (int)((float)idx * 100f / (float)len);
|
|
if (newPct != pctParsed)
|
|
{
|
|
pctParsed = newPct;
|
|
}
|
|
cur = json[idx];
|
|
return cur;
|
|
}
|
|
|
|
private void SkipWs()
|
|
{
|
|
const string ws = " \n\t\r";
|
|
while (ws.IndexOf(cur) != -1) Next();
|
|
}
|
|
|
|
private string PosMsg()
|
|
{
|
|
return "line " + line.ToString() + ", column " + linechar.ToString();
|
|
}
|
|
|
|
private JsonValue ParseValue()
|
|
{
|
|
// Skip spaces
|
|
SkipWs();
|
|
|
|
switch (cur)
|
|
{
|
|
case '[':
|
|
return ParseArray();
|
|
case '{':
|
|
return ParseDict();
|
|
case '"':
|
|
return ParseString();
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
return ParseNumber();
|
|
case 't':
|
|
case 'f':
|
|
case 'n':
|
|
return ParseConstant();
|
|
default:
|
|
throw new JSONParseException("Cannot parse json value starting with '" + json.Substring(idx, 5) + "' at " + PosMsg());
|
|
}
|
|
}
|
|
|
|
private JsonValue ParseArray()
|
|
{
|
|
Next();
|
|
SkipWs();
|
|
List<JsonValue> arr = new List<JsonValue>();
|
|
while (cur != ']')
|
|
{
|
|
arr.Add(ParseValue());
|
|
SkipWs();
|
|
if (cur == ',')
|
|
{
|
|
Next();
|
|
SkipWs();
|
|
}
|
|
}
|
|
Next();
|
|
return new JsonValue(arr);
|
|
}
|
|
|
|
private JsonValue ParseDict()
|
|
{
|
|
Next();
|
|
SkipWs();
|
|
Dictionary<string, JsonValue> dict = new Dictionary<string, JsonValue>();
|
|
while (cur != '}')
|
|
{
|
|
JsonValue key = ParseValue();
|
|
if (!key.IsString())
|
|
throw new JSONParseException("Key not string type at " + PosMsg());
|
|
SkipWs();
|
|
if (cur != ':')
|
|
throw new JSONParseException("Missing dict entry delimiter ':' at " + PosMsg());
|
|
Next();
|
|
dict.Add(key.AsString(), ParseValue());
|
|
SkipWs();
|
|
if (cur == ',')
|
|
{
|
|
Next();
|
|
SkipWs();
|
|
}
|
|
}
|
|
Next();
|
|
return new JsonValue(dict);
|
|
}
|
|
|
|
static char[] endcodes = { '\\', '"' };
|
|
|
|
private JsonValue ParseString()
|
|
{
|
|
string res = "";
|
|
|
|
Next();
|
|
|
|
while (idx < len)
|
|
{
|
|
int endidx = json.IndexOfAny(endcodes, idx);
|
|
if (endidx < 0)
|
|
throw new JSONParseException("missing '\"' to end string at " + PosMsg());
|
|
|
|
res += json.Substring(idx, endidx - idx);
|
|
|
|
if (json[endidx] == '"')
|
|
{
|
|
cur = json[endidx];
|
|
idx = endidx;
|
|
break;
|
|
}
|
|
|
|
endidx++; // get escape code
|
|
if (endidx >= len)
|
|
throw new JSONParseException("End of json while parsing while parsing string at " + PosMsg());
|
|
|
|
// char at endidx is \
|
|
char ncur = json[endidx];
|
|
switch (ncur)
|
|
{
|
|
case '"':
|
|
goto case '/';
|
|
case '\\':
|
|
goto case '/';
|
|
case '/':
|
|
res += ncur;
|
|
break;
|
|
case 'b':
|
|
res += '\b';
|
|
break;
|
|
case 'f':
|
|
res += '\f';
|
|
break;
|
|
case 'n':
|
|
res += '\n';
|
|
break;
|
|
case 'r':
|
|
res += '\r';
|
|
break;
|
|
case 't':
|
|
res += '\t';
|
|
break;
|
|
case 'u':
|
|
// Unicode char specified by 4 hex digits
|
|
string digit = "";
|
|
if (endidx + 4 >= len)
|
|
throw new JSONParseException("End of json while parsing while parsing unicode char near " + PosMsg());
|
|
digit += json[endidx + 1];
|
|
digit += json[endidx + 2];
|
|
digit += json[endidx + 3];
|
|
digit += json[endidx + 4];
|
|
try
|
|
{
|
|
int d = Int32.Parse(digit, System.Globalization.NumberStyles.AllowHexSpecifier);
|
|
res += (char)d;
|
|
}
|
|
catch (FormatException)
|
|
{
|
|
throw new JSONParseException("Invalid unicode escape char near " + PosMsg());
|
|
}
|
|
endidx += 4;
|
|
break;
|
|
default:
|
|
throw new JSONParseException("Invalid escape char '" + ncur + "' near " + PosMsg());
|
|
}
|
|
idx = endidx + 1;
|
|
}
|
|
if (idx >= len)
|
|
throw new JSONParseException("End of json while parsing while parsing string near " + PosMsg());
|
|
|
|
cur = json[idx];
|
|
|
|
Next();
|
|
return new JsonValue(res);
|
|
}
|
|
|
|
private JsonValue ParseNumber()
|
|
{
|
|
string resstr = "";
|
|
|
|
if (cur == '-')
|
|
{
|
|
resstr = "-";
|
|
Next();
|
|
}
|
|
|
|
while (cur >= '0' && cur <= '9')
|
|
{
|
|
resstr += cur;
|
|
Next();
|
|
}
|
|
if (cur == '.')
|
|
{
|
|
Next();
|
|
resstr += '.';
|
|
while (cur >= '0' && cur <= '9')
|
|
{
|
|
resstr += cur;
|
|
Next();
|
|
}
|
|
}
|
|
|
|
if (cur == 'e' || cur == 'E')
|
|
{
|
|
resstr += "e";
|
|
Next();
|
|
if (cur != '-' && cur != '+')
|
|
{
|
|
// throw new JSONParseException("Missing - or + in 'e' potent specifier at " + PosMsg());
|
|
resstr += cur;
|
|
Next();
|
|
}
|
|
while (cur >= '0' && cur <= '9')
|
|
{
|
|
resstr += cur;
|
|
Next();
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
float f = Convert.ToSingle(resstr);
|
|
return new JsonValue(f);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
throw new JSONParseException("Cannot convert string to float : '" + resstr + "' at " + PosMsg());
|
|
}
|
|
}
|
|
|
|
private JsonValue ParseConstant()
|
|
{
|
|
string c = "" + cur + Next() + Next() + Next();
|
|
Next();
|
|
if (c == "true")
|
|
{
|
|
return new JsonValue(true);
|
|
}
|
|
else if (c == "fals")
|
|
{
|
|
if (cur == 'e')
|
|
{
|
|
Next();
|
|
return new JsonValue(false);
|
|
}
|
|
}
|
|
else if (c == "null")
|
|
{
|
|
return new JsonValue(null);
|
|
}
|
|
throw new JSONParseException("Invalid token at " + PosMsg());
|
|
}
|
|
};
|
|
} |