Skip to content

Instantly share code, notes, and snippets.

@ctigeek ctigeek/Tjson.cs
Created Jan 24, 2019

Embed
What would you like to do?
Tjson Deserializer....
// written by Steven Swenson
// Released under the MIT open source license...
// No warranty at all. use at your own risk.
public static class Tjson
{
//https://gist.github.com/ctigeek/4950c4b07a50a0791f012858e3bb2214
private static readonly Splitter.Preserver QuotePreserver = new Splitter.Preserver { Start = '"', End = '"', Escape = '\\' };
private static readonly Splitter.Preserver CurlyBoiPreserver = new Splitter.Preserver { Start = '{', End = '}', Escape = '\\', Recursive = true, SubPreserver = QuotePreserver };
private static readonly Splitter.Preserver BracketPreserver = new Splitter.Preserver { Start = '[', End = ']', Escape = '\\', Recursive = true, SubPreserver = QuotePreserver };
public static T Deserialize<T>(string literal) where T: new()
{
var obj = new T();
return (T) Deserialize(literal, typeof(T), obj);
}
public static object Deserialize(string literal, Type type)
{
var obj = Activator.CreateInstance(type.Assembly.FullName, type.FullName).Unwrap();
return Deserialize(literal, type, obj);
}
private static object Deserialize(string literal, Type type, object obj)
{
var nvps = ParseObjectIntoStrings(literal.Trim());
var props = type.GetProperties();
var fields = type.GetFields();
foreach (var part in nvps)
{
var nvp = ParseNvpIntoStrings(part);
var prop1 = props.FirstOrDefault(p => p.Name == nvp.Name);
if (prop1 != null)
{
prop1.SetValue(obj, CreateNetObject(nvp, prop1.PropertyType));
}
else
{
var fld1 = fields.FirstOrDefault(f => f.Name == nvp.Name);
if (fld1 != null)
{
fld1.SetValue(obj, CreateNetObject(nvp, fld1.FieldType));
}
}
}
return obj;
}
private static object CreateNetObject(NVP nvp, Type propType)
{
if (nvp.ValueType == TjsonType.TjsonObject)
{
var subObject = Deserialize(nvp.Value, propType);
return subObject;
}
if (nvp.ValueType == TjsonType.TjsonSeries)
{
var array = ParseArray(nvp.Series, nvp.Value, propType);
return array;
}
var value = TurnValueIntoObject(nvp);
return value;
}
private static object TurnValueIntoObject(NVP nvp)
{
switch (nvp.ValueType)
{
case TjsonType.TjsonString:
return nvp.Value;
case TjsonType.TjsonBool:
return bool.Parse(nvp.Value);
case TjsonType.TjsonTime:
return ParseRfc3339DateTime(nvp.Value);
case TjsonType.TjsonInt:
var lng = long.Parse(nvp.Value);
if (lng <= int.MaxValue)
{
return (int)lng;
}
return lng;
case TjsonType.TjsonUint:
var ulng = ulong.Parse(nvp.Value);
if (ulng <= uint.MaxValue)
{
return (uint)ulng;
}
return ulng;
case TjsonType.TjsonFloat:
return float.Parse(nvp.Value);
case TjsonType.TjsonObject:
return nvp.Value;
case TjsonType.TjsonBase16:
return Encoding.ASCII.GetBytes(nvp.Value);
case TjsonType.TjsonBase32:
return DecodeBase32(nvp.Value);
case TjsonType.TjsonBase64:
return Convert.FromBase64String(nvp.Value);
default:
throw new Exception("This can never happen.");
}
}
private static object ParseArray(Series series, string value, Type arrayType)
{
if (value[0] != '[' || value[value.Length - 1] != ']')
{
throw new Exception("Invalid tjson: array must start and end with brackets.");
}
var noBrackets = value.Substring(1, value.Length - 2).Trim();
bool shouldRemoveQuotes = ShouldRemoveQuotes(series.ElementType);
var arrayParts = Splitter.Split(noBrackets, ',', '\0', QuotePreserver, CurlyBoiPreserver, BracketPreserver)
.Select(e => shouldRemoveQuotes ? RemoveQuotes(e.Trim()) : e.Trim()).ToArray();
//todo: verify set has unique elements
var array = Activator.CreateInstance(arrayType, arrayParts.Length) as Array;
int arrayIndex = 0;
foreach (var part in arrayParts)
{
if (series.ElementType == TjsonType.TjsonSeries)
{
var subarray = ParseArray(series.SubSeries, part, arrayType.GetElementType());
array.SetValue(subarray, arrayIndex);
}
else if (series.ElementType == TjsonType.TjsonObject)
{
var obj = Deserialize(part, arrayType.GetElementType());
array.SetValue(obj, arrayIndex);
}
else
{
var obj = TurnValueIntoObject(new NVP { Value = part, ValueType = series.ElementType });
array.SetValue(obj, arrayIndex);
}
arrayIndex++;
}
return array;
}
private static byte[] DecodeBase32(string input)
{
string ValidBase32Chars = "QAZ2WSX3EDC4RFV5TGB6YHN7UJM8K9LP";
input = input.TrimEnd('='); //remove padding characters
int byteCount = input.Length * 5 / 8; //this must be TRUNCATED
byte[] returnArray = new byte[byteCount];
byte curByte = 0, bitsRemaining = 8;
int mask = 0, arrayIndex = 0;
foreach (char c in input)
{
int cValue = (int)c;
//65-90 == uppercase letters
if (cValue < 91 && cValue > 64)
{
cValue = cValue - 65;
}
//50-55 == numbers 2-7
if (cValue < 56 && cValue > 49)
{
cValue = cValue - 24;
}
//97-122 == lowercase letters
if (cValue < 123 && cValue > 96)
{
cValue = cValue - 97;
}
if (bitsRemaining > 5)
{
mask = cValue << (bitsRemaining - 5);
curByte = (byte)(curByte | mask);
bitsRemaining -= 5;
}
else
{
mask = cValue >> (5 - bitsRemaining);
curByte = (byte)(curByte | mask);
returnArray[arrayIndex++] = curByte;
curByte = (byte)(cValue << (3 + bitsRemaining));
bitsRemaining += 3;
}
}
//if we didn't end with a full byte
if (arrayIndex != byteCount)
{
returnArray[arrayIndex] = curByte;
}
return returnArray;
}
private static readonly string[] Rfc3339DateTimePatterns = new[]
{
"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffK",
"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'ffffffK",
"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffK",
"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'ffffK",
"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK",
"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'ffK",
"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fK",
"yyyy'-'MM'-'dd'T'HH':'mm':'ssK",
"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffK"
};
private static DateTime ParseRfc3339DateTime(string value)
{
if (DateTime.TryParseExact(value, Rfc3339DateTimePatterns, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var parseResult))
{
parseResult = DateTime.SpecifyKind(parseResult, DateTimeKind.Utc);
return parseResult;
}
throw new Exception("Invalid TJSON. DateTimes must be in Rfc3339 format. This is invalid: `" + value + "`.");
}
private static NVP ParseNvpIntoStrings(string nvpString)
{
var results = Splitter.Split(nvpString, ':', '\0', QuotePreserver, BracketPreserver, CurlyBoiPreserver)
.Select(e => e.Trim()).ToArray();
if (results.Length != 2)
{
throw new Exception("Invalid JSON document. Error parsing for ':'.");
}
var nameWithoutQuotes = results[0].Substring(1, results[0].Length - 2);
var nameTypeParts = nameWithoutQuotes.Split(':');
if (nameTypeParts.Length != 2)
{
throw new Exception("Invalid TJSON document. Name must include type.");
}
var nvp = new NVP
{
Name = nameTypeParts[0].Replace("-", "_"),
Value = results[1],
ValueType = GetTjsonType(nameTypeParts[1])
};
if (nvp.ValueType == TjsonType.TjsonSeries)
{
nvp.Series = ParseSeriesTypes(nameTypeParts[1]);
}
if (ShouldRemoveQuotes(nvp.ValueType))
{
nvp.Value = RemoveQuotes(nvp.Value);
}
return nvp;
}
private static Series ParseSeriesTypes(string typeId)
{
var series = new Series();
if (typeId[0] == 'A')
{
series.SeriesType = TjsonSeriesType.TjsonArray;
}
else if (typeId[0] == 'S')
{
series.SeriesType = TjsonSeriesType.TjsonSet;
}
else
{
throw new Exception("Unknown series type.");
}
var removeAO = typeId.Substring(1);
if (!(removeAO.StartsWith("<") && removeAO.EndsWith(">")))
{
throw new Exception("Invalid TJSON: Incorrect type identifier:" + typeId);
}
if (removeAO == "<>")
{
series.ElementType = TjsonType.TjsonObject;
return series;
}
var removeChops = removeAO.Substring(1, removeAO.Length - 2); //removeChops will now either be a simple type, an object, or another series....
series.ElementType = GetTjsonType(removeChops);
if (series.ElementType == TjsonType.TjsonSeries)
{
series.SubSeries = ParseSeriesTypes(removeChops);
}
return series;
}
private static TjsonType GetTjsonType(string typeId)
{
if (typeId.StartsWith("A<") || typeId.StartsWith("S<"))
{
return TjsonType.TjsonSeries;
}
switch (typeId)
{
case "s":
return TjsonType.TjsonString;
case "O":
return TjsonType.TjsonObject;
case "d":
return TjsonType.TjsonBase64;
case "d16":
return TjsonType.TjsonBase16;
case "d32":
return TjsonType.TjsonBase32;
case "d64":
return TjsonType.TjsonBase64;
case "i":
return TjsonType.TjsonInt;
case "u":
return TjsonType.TjsonUint;
case "f":
return TjsonType.TjsonFloat;
case "b":
return TjsonType.TjsonBool;
case "t":
return TjsonType.TjsonTime;
default:
throw new Exception("Unknown TJSON type: " + typeId);
}
}
private static string[] ParseObjectIntoStrings(string literal)
{
if (literal[0] != '{' || literal[literal.Length - 1] != '}')
{
throw new Exception("Invalid JSON object. Missing opening or closing bracket.");
}
literal = literal.Substring(1, literal.Length - 2);
var results = Splitter.Split(literal, ',', '\0', QuotePreserver, BracketPreserver, CurlyBoiPreserver)
.Select(e => e.Trim()).ToArray();
return results;
}
private static bool ShouldRemoveQuotes(TjsonType tjsonType)
{
return (tjsonType == TjsonType.TjsonString || tjsonType == TjsonType.TjsonBase16 ||
tjsonType == TjsonType.TjsonBase32 || tjsonType == TjsonType.TjsonBase64 ||
tjsonType == TjsonType.TjsonFloat || tjsonType == TjsonType.TjsonInt ||
tjsonType == TjsonType.TjsonUint || tjsonType == TjsonType.TjsonTime);
}
private static string RemoveQuotes(string value)
{
if (value[0] == '"' && value[value.Length - 1] == '"')
{
return value.Substring(1, value.Length - 2);
}
throw new Exception("Invalid TJSON: The following value must be wrapped in quotes: \r\n" + value);
}
private enum TjsonType
{
TjsonString,
TjsonObject,
TjsonSeries,
TjsonBase16,
TjsonBase32,
TjsonBase64,
TjsonInt,
TjsonUint,
TjsonFloat,
TjsonBool,
TjsonTime
}
private enum TjsonSeriesType
{
TjsonArray,
TjsonSet
}
//Dead code...
private static Type GetTypeFromTjsonType(TjsonType tjsonType)
{
switch (tjsonType)
{
case TjsonType.TjsonString:
return typeof(string);
case TjsonType.TjsonObject:
return typeof(object);
case TjsonType.TjsonBool:
return typeof(bool);
case TjsonType.TjsonTime:
return typeof(DateTime);
case TjsonType.TjsonBase16:
case TjsonType.TjsonBase32:
case TjsonType.TjsonBase64:
return typeof(byte[]);
case TjsonType.TjsonInt:
return typeof(long);
case TjsonType.TjsonUint:
return typeof(ulong);
case TjsonType.TjsonFloat:
return typeof(float);
default:
throw new Exception("ermagawd");
}
}
private class NVP
{
public string Name;
public string Value;
public TjsonType ValueType;
public Series Series;
}
private class Series
{
//we might need the hard type here too to track during recursive calls
public TjsonSeriesType SeriesType;
public TjsonType ElementType;
public Series SubSeries;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.