public static T Path <T>(this JsonObj obj, string target, T replacement) { try { return(Path <T>(obj, target)); } catch (KeyNotFoundException) { return(replacement); } }
/// <summary> /// Parses an input string into an object. The input can be any well-formed JSON or JSON5. /// </summary> /// <param name="text">The string to parse.</param> /// <returns>A JsonObj, list, string, double...</returns> public static object Parse(string text) { var previousCulture = System.Threading.Thread.CurrentThread.CurrentCulture; System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture; var at = 0; //The index of the current character var ch = ' '; //The current character var escapee = new Dictionary <char, string> { { '\'', "\'" }, { '\"', "\"" }, { '\\', "\\" }, { '/', "/" }, { '\n', string.Empty }, //Replace escaped newlines in strings w/ empty string { 'b', "\b" }, { 'f', "\f" }, { 'n', "\n" }, { 'r', "\r" }, { 't', "\t" }, }; Func <int> locateError = () => { var line = 0; for (var i = 0; i < at; i++) { if (text[i] == '\r') { line++; } } return(line); }; Func <char> next = () => { if (at >= text.Length) { ch = '\0'; } else { ch = text[at]; at++; } return(ch); }; Func <char, char> expect = (c) => { if (c != ch) { throw new JsonException(string.Format("Expected '{1}' instead of '{0}'", ch, c), locateError()); } return(next()); }; #region identifier Func <string> identifier = () => { var key = new StringBuilder(); key.Append(ch); //Identifiers must start with a letter, _ or $. if ((ch != '_' && ch != '$') && (ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z')) { throw new JsonException("Bad identifier", locateError()); } //Subsequent characters can contain digits. while (next() != '\0' && ( ch == '_' || ch == '$' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9'))) { key.Append(ch); } return(key.ToString()); }; #endregion #region number Func <object> number = () => { var sign = '\0'; var str = new StringBuilder(); var bas = 10; if (ch == '-' || ch == '+') { sign = ch; str.Append(ch); expect(ch); } if (ch == 'I') { expect('I'); expect('n'); expect('f'); expect('i'); expect('n'); expect('i'); expect('t'); expect('y'); if (!AllowNaN) { throw new JsonException("Found an unallowed Infinity value."); } return((sign == '-') ? double.NegativeInfinity : double.PositiveInfinity); } if (ch == '0') { str.Append(ch); next(); if (ch == 'x' || ch == 'X') { str.Append(ch); next(); bas = 16; } else if (ch >= '0' && ch <= '9') { throw new JsonException("Octal literal", locateError()); } } //https://github.com/aseemk/json5/issues/36 if (bas == 16 && sign != '\0') { throw new JsonException("Signed hexadecimal literal", locateError()); } switch (bas) { case 10: while (ch >= '0' && ch <= '9') { str.Append(ch); next(); } if (ch == '.') { if (str.Length == 0) { str.Append('0'); } str.Append(ch); while (next() != '\0' && ch >= '0' && ch <= '9') { str.Append(ch); } } if (ch == 'e' || ch == 'E') { str.Append(ch); next(); if (ch == '-' || ch == '+') { str.Append(ch); next(); } while (ch >= '0' && ch <= '9') { str.Append(ch); next(); } } break; case 16: while (ch >= '0' && ch <= '9' || ch >= 'A' && ch <= 'F' || ch >= 'a' && ch <= 'f') { str.Append(ch); next(); } break; default: throw new JsonException("Invalid number base, somehow."); } if (bas == 16) { return(int.Parse(str.ToString().Substring(2), System.Globalization.NumberStyles.HexNumber)); } try { return(Double.Parse(str.ToString())); } catch (OverflowException) { return((sign == '-') ? Double.MinValue : Double.MaxValue); } }; #endregion #region string Func <string> @string = () => { var hex = 0; var i = 0; var str = new StringBuilder(); char delim; var uffff = 0; //When parsing for string values, we must look for ' or " and \ characters. if (ch == '"' || ch == '\'') { delim = ch; while (next() != '\0') { if (ch == delim) { next(); return(str.ToString()); } else if (ch == '\\') { next(); if (ch == '\r' || ch == '\n') { str.Append('\n'); next(); continue; } if (ch == 'u') { uffff = 0; for (i = 0; i < 4; i += 1) { hex = int.Parse(next().ToString(), System.Globalization.NumberStyles.HexNumber); uffff = uffff * 16 + hex; } str.Append((char)uffff); } else if (escapee.ContainsKey(ch)) { str.Append(escapee[ch]); } else { break; } } else { str.Append(ch); } } } throw new JsonException("Bad string", locateError()); }; #endregion #region inlineComment Func <string> inlineComment = () => { //Skip an inline comment, assuming this is one. The current character should //be the second / character in the // pair that begins this inline comment. //To finish the inline comment, we look for a newline or the end of the text. if (ch != '/') { throw new JsonException("Not an inline comment"); } do { next(); if (ch == '\n') { expect('\n'); return(string.Empty); } } while (ch != '\0'); return(string.Empty); }; #endregion #region blockComment Func <string> blockComment = () => { //Skip a block comment, assuming this is one. The current character should be //the * character in the /* pair that begins this block comment. //To finish the block comment, we look for an ending */ pair of characters, //but we also watch for the end of text before the comment is terminated. if (ch != '*') { throw new JsonException("Not a block comment"); } do { next(); while (ch == '*') { expect('*'); if (ch == '/') { expect('/'); return(string.Empty); } } } while (ch != '\0'); throw new JsonException("Unterminated block comment"); }; #endregion #region comment Func <string> comment = () => { if (ch != '/') { throw new JsonException("Not a comment", locateError()); } expect('/'); if (ch == '/') { inlineComment(); } else if (ch == '*') { blockComment(); } else { throw new JsonException("Unrecognized comment", locateError()); } return(string.Empty); }; #endregion #region white Func <string> white = () => { //Skip whitespace and comments. //Note that we're detecting comments by only a single / character. //This works since regular expressions are not valid JSON(5), but this will //break if there are other valid values that begin with a / character! while (ch != '\0') { if (ch == '/') { comment(); } else if (ch <= ' ') { next(); } else { return(string.Empty); } } return(string.Empty); }; #endregion #region word Func <bool?> word = () => { //true, false, or null. switch (ch) { case 't': expect('t'); expect('r'); expect('u'); expect('e'); return(true); case 'f': expect('f'); expect('a'); expect('l'); expect('s'); expect('e'); return(false); case 'n': expect('n'); expect('u'); expect('l'); expect('l'); return(null); default: throw new JsonException(string.Format("Unexpected '{0}'", ch), locateError()); } }; #endregion Func <object> value = null; //Place holder for the value function. #region array Func <List <object> > @array = () => { var justHadComma = false; var arr = new List <object>(); if (ch == '[') { expect('['); white(); while (ch != '\0') { if (ch == ']') { if (!AllowTrailingComma && justHadComma) { throw new JsonException("Superfluous trailing comma", locateError()); } expect(ch); return(arr); //.ToArray(); //Potentially empty array } //ES5 allows omitting elements in arrays, e.g. [,] and //[,null]. We don't allow this in JSON5. if (ch == ',') { throw new JsonException("Missing array element", locateError()); } else { arr.Add(value()); } white(); //If there's no comma after this value, this needs to //be the end of the array. if (ch != ',') { expect(']'); return(arr); } expect(','); justHadComma = true; white(); } } throw new JsonException("Bad array", locateError()); }; #endregion #region object Func <JsonObj> @object = () => { //Parse an object value. var key = string.Empty; var obj = new JsonObj(); if (ch == '{') { expect('{'); white(); while (ch != '\0') { if (ch == '}') { expect('}'); return(obj); //Potentially empty object } //Keys can be unquoted. If they are, they need to be //valid JS identifiers. if (ch == '\"' || ch == '\'') { key = @string(); } else { key = identifier(); } white(); expect(':'); if (obj.ContainsKey(key)) { throw new JsonException(string.Format("Duplicate key \"{0}\"", key), locateError()); } obj[key] = value(); white(); //If there's no comma after this pair, this needs to be //the end of the object. if (ch != ',') { expect('}'); return(obj); } expect(','); white(); } } throw new JsonException("Bad object", locateError()); }; #endregion #region value value = () => { //Parse a JSON value. It could be an object, an array, a string, a number, //or a word. white(); switch (ch) { case '{': return(@object()); case '[': return(@array()); case '\"': case '\'': return(@string()); case '-': case '+': case '.': return(number()); case 'N': expect('N'); expect('a'); expect('N'); if (!AllowNaN) { throw new JsonException("Found an unallowed NaN value."); } return(double.NaN); case 'I': expect('I'); expect('n'); expect('f'); expect('i'); expect('n'); expect('i'); expect('t'); expect('y'); if (!AllowNaN) { throw new JsonException("Found an unallowed Infinity value."); } return(double.PositiveInfinity); default: return(ch >= '0' && ch <= '9' ? @number() : word()); } }; #endregion //wow. //much cheat. //so unexpected. //-- KAWA white(); object ret = null; if (ch == '\0') { //Ret is null and stays null. } else if (ch == '[') { ret = @array(); } else if (ch == '{') { ret = @object(); } else { ret = value(); } System.Threading.Thread.CurrentThread.CurrentCulture = previousCulture; return(ret); }
public static bool IsValid(JsonObj data, JsonObj schema, JsonObj node = null) { if (node == null) { SchemaError = SchemaErrors.None; if (!schema["type"].Equals("object")) { throw new JsonException("Can only validate objects."); } return(IsValid(data, schema, schema)); } var properties = node["properties"] as JsonObj; if (node.ContainsKey("required")) { foreach (var requirement in (node["required"] as List <object>).Select(i => i.ToString())) { if (!data.ContainsKey(requirement)) { SchemaError = SchemaErrors.MissingRequirement; return(false); } } } foreach (var property in properties) { var key = property.Key; var value = property.Value as JsonObj; var type = value["type"] as string; if (data.ContainsKey(key)) { var item = data[key]; if (!typesMatch(type, item)) { SchemaError = SchemaErrors.TypeMismatch; return(false); } if (type == "object") { if (!IsValid(item as JsonObj, schema, value)) { return(false); } } if (type == "array") { var array = (IEnumerable <object>)item; if (value.ContainsKey("items")) { var items = value["items"] as JsonObj; var itemType = items["type"] as string; foreach (var thing in array) { if (!typesMatch(itemType, thing)) { SchemaError = SchemaErrors.TypeMismatch; return(false); } } } if (value.ContainsKey("maxItems") && array.Count() >= Convert.ToInt64(value["maxItems"])) { return(false); } if (value.ContainsKey("minItems") && array.Count() < Convert.ToInt64(value["minItems"])) { return(false); } if (value.ContainsKey("uniqueItems") && Convert.ToBoolean(value["uniqueItems"])) { var distinct = array.Distinct(); if (array.Count() != distinct.Count()) { return(false); } } } if (type == "string") { if (value.ContainsKey("pattern") && !Regex.IsMatch(item as string, value["pattern"] as string, RegexOptions.IgnoreCase)) { SchemaError = SchemaErrors.PatternMismatch; return(false); } if (value.ContainsKey("maxLength") && (item as string).Length > Convert.ToInt64(value["maxLength"])) { return(false); } if (value.ContainsKey("minLength") && (item as string).Length < Convert.ToInt64(value["minLength"])) { return(false); } } if (type == "integer" || type == "number") { var lastError = SchemaError; SchemaError = SchemaErrors.NumberMismatch; var val = Convert.ToDouble(item); if (value.ContainsKey("multipleOf") && val % Convert.ToDouble(value["multipleOf"]) != 0) { return(false); } if (value.ContainsKey("maximum")) { var max = Convert.ToDouble(value["maximum"]); if (value.ContainsKey("exclusiveMaximum") && Convert.ToBoolean(value["exclusiveMaximum"]) && val >= max) { return(false); } else if (val > max) { return(false); } } if (value.ContainsKey("minimum")) { var min = Convert.ToDouble(value["minimum"]); if (value.ContainsKey("exclusiveMinimum") && Convert.ToBoolean(value["exclusiveMinimum"]) && val <= min) { return(false); } else if (val < min) { return(false); } } SchemaError = lastError; } } } return(true); }
/// <summary> /// Returns an object of type T found in the specified path. /// </summary> /// <typeparam name="T">The type of object to return.</typeparam> /// <param name="obj">The JsonObj to look through.</param> /// <param name="target">The path to follow.</param> /// <returns>The object at the end of the path.</returns> /// <remarks> /// If obj is a Starbound Versioned JSON object, if the first key is not found, /// Path will automatically try skipping into the __content object. /// </remarks> public static T Path <T>(this JsonObj obj, string target) { if (string.IsNullOrWhiteSpace(target)) { throw new ArgumentException("Path is empty."); } if (!target.StartsWith("/")) { throw new ArgumentException("Path does not start with root."); } if (target.EndsWith("/")) { throw new ArgumentException("Path does not end with a key or index."); } var parts = target.Substring(1).Split('/'); object root = obj; var here = root; int index; foreach (var part in parts) { if (part == "-") { throw new JsonException("Can't use - here; we're not patching anything."); } bool isIndex = int.TryParse(part, out index); if (isIndex && here == root) { throw new JsonException("Tried to start with an array index. That's extra special."); } if (isIndex && here is object[]) { var list = (object[])here; if (index < 0 || index >= list.Length) { throw new JsonException("Index out of range."); } here = list[index]; } else if (isIndex && here is List <object> ) { var list = (List <object>)here; if (index < 0 || index >= list.Count) { throw new IndexOutOfRangeException(); } here = list[index]; } else if (here is JsonObj) { var map = (JsonObj)here; if (here == root && map.ContainsKey("__content") && !map.ContainsKey(part)) { //Sneakily stealthily skip into this. map = (JsonObj)map["__content"]; } if (!map.ContainsKey(part)) { throw new KeyNotFoundException(); } here = map[part]; } else { throw new JsonException("Current node is not an array or object, but path isn't done yet."); } } if (typeof(T).Name == "Int32" && here is Int64) { here = (int)(Int64)here; } if (typeof(T).Name == "Boolean" && here is bool) { here = (bool)here; } else if (typeof(T).Name == "Int32[]" && here is List <object> ) { here = ((List <object>)here).Select(x => (int)(Int64)x).ToArray(); } else if (typeof(T).Name == "Int64[]" && here is List <object> ) { here = ((List <object>)here).Select(x => (Int64)x).ToArray(); } else if (typeof(T).Name == "Boolean[]" && here is List <object> ) { here = ((List <object>)here).Select(x => (bool)x).ToArray(); } else if (typeof(T).Name == "String[]" && here is List <object> ) { here = ((List <object>)here).Select(x => (string)x).ToArray(); } else if (typeof(T).Name == "JsonObj[]" && here is List <object> ) { here = ((List <object>)here).Cast <JsonObj>().ToArray(); } else if (typeof(T).Name == "Object[]" && here is List <object> ) { here = ((List <object>)here).ToArray(); } else if (typeof(T).Name == "List`1") { var contained = typeof(T).GetGenericArguments()[0]; var hereList = (List <object>)here; switch (contained.Name) { case "Int32": here = hereList.Select(x => (int)(Int64)x).ToList(); break; case "Boolean": here = hereList.Select(x => (bool)x).ToList(); break; case "String": here = hereList.Select(x => (string)x).ToList(); break; case "JsonObj": here = hereList.Select(x => (JsonObj)x).ToList(); break; default: here = hereList; break; } } if (!(here is T)) { throw new JsonException(string.Format("Value at end of path is not of the requested type -- found {0} but expected {1}.", here.GetType(), typeof(T))); } return((T)here); }
/// <summary> /// Returns the JsonObj found in the specified path. /// </summary> /// <param name="obj">The JsonObj to look through.</param> /// <param name="target">The path to follow.</param> /// <returns>The JsonObj at the end of the path.</returns> /// <remarks> /// If obj is a Starbound Versioned JSON object, if the first key is not found, /// Path will automatically try skipping into the __content object. /// </remarks> public static JsonObj Path(this JsonObj obj, string target) { return(Path <JsonObj>(obj, target)); }
/// <summary> /// Returns the JsonObj found in the specified path. /// </summary> /// <param name="obj">The JsonObj to look through.</param> /// <param name="path">The path to follow.</param> /// <returns>The JsonObj at the end of the path.</returns> /// <remarks> /// If obj is a Starbound Versioned JSON object, if the first key is not found, /// Path will automatically try skipping into the __content object. /// </remarks> public static JsonObj Path(this JsonObj obj, string path) { return(Path <JsonObj>(obj, path)); }
/// <summary> /// Parses Starbound Versioned JSON from a stream into an object. The input stream can be headered or bare. If the input stream is headered, the resulting object will be wrapped to preserve versioning information. /// </summary> /// <param name="stream">The input stream to parse from.</param> /// <returns>A JsonObj, list, string, double...</returns> public static object Deserialize(BinaryReader stream) { Func <object> something = null; //placeholder for @array and @object Func <double> @double = () => { var moto8 = stream.ReadBytes(8); var intel8 = new[] { moto8[7], moto8[6], moto8[5], moto8[4], moto8[3], moto8[2], moto8[1], moto8[0] }; var ret = 0.0; using (var intel = new BinaryReader(new MemoryStream(intel8))) ret = intel.ReadDouble(); return(ret); }; Func <bool> @bool = () => { return(stream.ReadBoolean()); }; Func <long> @int = () => { return(stream.ReadVLQSigned()); }; Func <string> @string = () => { var len = (int)stream.ReadVLQUnsigned(); var str = Encoding.UTF8.GetString(stream.ReadBytes(len)); return(str); }; Func <object> @array = () => { var count = (int)stream.ReadVLQUnsigned(); var ret = new object[count]; for (var i = 0; i < count; i++) { ret[i] = @something(); } var allAreNull = ret.All(i => i == null); if (!allAreNull) { var allAreSameType = true; foreach (var i in ret) { if (i == null) { continue; } if (!(i is string)) { allAreSameType = false; break; } } if (allAreSameType) { return(ret.Cast <string>().ToList()); } allAreSameType = true; foreach (var i in ret) { if (i == null) { continue; } if (!(i is int)) { allAreSameType = false; break; } } if (allAreSameType) { return(ret.Cast <int>().ToList()); } allAreSameType = true; foreach (var i in ret) { if (i == null) { continue; } if (!(i is double)) { allAreSameType = false; break; } } if (allAreSameType) { return(ret.Cast <double>().ToList()); } } return(ret.ToList()); }; Func <JsonObj> @object = () => { var count = (int)stream.ReadVLQUnsigned(); var ret = new JsonObj(); for (var i = 0; i < count; i++) { ret.Add(@string(), @something()); } return(ret); }; something = () => { var type = stream.ReadByte(); switch (type) { case 1: return(null); case 2: return(@double()); case 3: return(@bool()); case 4: return(@int()); case 5: return(@string()); case 6: return(@array()); case 7: return(@object()); default: throw new JsonException(string.Format("Unknown item type 0x{0:X2} while deserializing. Stream offset: 0x{1:X}", type, stream.BaseStream.Position)); } }; if ((char)stream.PeekChar() == 'S') { //SBVJ file? var sbvj = new string(stream.ReadChars(6)); if (sbvj != "SBVJ01") { throw new JsonException("File does not start with a valid object identifier, nor is it a Starbound Versioned JSON file."); } var identifier = @string(); var hasVersion = @bool(); var version = 0; if (hasVersion) { var moto4 = stream.ReadBytes(4); var intel4 = new[] { moto4[3], moto4[2], moto4[1], moto4[0] }; using (var intel = new BinaryReader(new MemoryStream(intel4))) { version = intel.ReadInt32(); } } var wrapper = new JsonObj(); wrapper["__id"] = identifier; if (hasVersion) { wrapper["__version"] = version; } wrapper["__content"] = something(); return(wrapper); } return(something()); }