Skip to content

Instantly share code, notes, and snippets.

@ngrebenshikov
Last active November 8, 2019 03:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ngrebenshikov/e8da157abdfbc0837f3fd8a4605d3db9 to your computer and use it in GitHub Desktop.
Save ngrebenshikov/e8da157abdfbc0837f3fd8a4605d3db9 to your computer and use it in GitHub Desktop.
haxe.Unserializer uses Array<String> as a buffer instead of String
/*
* Copyright (C)2005-2019 Haxe Foundation
*
* 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.
*/
package haxe;
import StringTools;
import haxe.ds.List;
@:noDoc
typedef TypeResolver = {
function resolveClass(name:String):Class<Dynamic>;
function resolveEnum(name:String):Enum<Dynamic>;
}
/**
The `Unserializer` class is the complement to the `Serializer` class. It parses
a serialization `String` and creates objects from the contained data.
This class can be used in two ways:
- create a `new Unserializer()` instance with a given serialization
String, then call its `unserialize()` method until all values are
extracted
- call `Unserializer.run()` to unserialize a single value from a given
String
The specification of the serialization format can be found here:
<https://haxe.org/manual/serialization/format>
**/
class Unserializer {
/**
This value can be set to use custom type resolvers.
A type resolver finds a `Class` or `Enum` instance from a given `String`.
By default, the Haxe `Type` Api is used.
A type resolver must provide two methods:
1. `resolveClass(name:String):Class<Dynamic>` is called to determine a
`Class` from a class name
2. `resolveEnum(name:String):Enum<Dynamic>` is called to determine an
`Enum` from an enum name
This value is applied when a new `Unserializer` instance is created.
Changing it afterwards has no effect on previously created instances.
**/
public static var DEFAULT_RESOLVER:TypeResolver = new DefaultResolver();
static var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789%:";
#if !neko
static var CODES = null;
static function initCodes() {
var codes = #if flash new flash.utils.ByteArray(); #else new Array(); #end
for (i in 0...BASE64.length)
codes[StringTools.fastCodeAt(BASE64, i)] = i;
return codes;
}
#end
var buf:Array<String> = [];
var pos:Int;
var length:Int;
var cache:Array<Dynamic>;
var scache:Array<String>;
var resolver:TypeResolver;
#if neko
var upos:Int;
#end
/**
Creates a new Unserializer instance, with its internal buffer
initialized to `buf`.
This does not parse `buf` immediately. It is parsed only when calls to
`this.unserialize` are made.
Each Unserializer instance maintains its own cache.
**/
public function new(buf:String) {
this.buf = buf.split("");
length = buf.length;
pos = 0;
#if neko
upos = 0;
#end
scache = new Array();
cache = new Array();
var r = DEFAULT_RESOLVER;
if (r == null) {
r = new DefaultResolver();
DEFAULT_RESOLVER = r;
}
resolver = r;
}
/**
Sets the type resolver of `this` Unserializer instance to `r`.
If `r` is `null`, a special resolver is used which returns `null` for all
input values.
See `DEFAULT_RESOLVER` for more information on type resolvers.
**/
public function setResolver(r) {
if (r == null)
resolver = NullResolver.instance;
else
resolver = r;
}
/**
Gets the type resolver of `this` Unserializer instance.
See `DEFAULT_RESOLVER` for more information on type resolvers.
**/
public function getResolver() {
return resolver;
}
inline function get(p):Int {
return buf[p].charCodeAt(0);
}
function readDigits() {
var k = 0;
var s = false;
var fpos = pos;
while (true) {
var c = get(pos);
if (StringTools.isEof(c))
break;
if (c == "-".code) {
if (pos != fpos)
break;
s = true;
pos++;
continue;
}
if (c < "0".code || c > "9".code)
break;
k = k * 10 + (c - "0".code);
pos++;
}
if (s)
k *= -1;
return k;
}
function readFloat() {
var p1 = pos;
while (true) {
var c = get(pos);
if (StringTools.isEof(c))
break;
// + - . , 0-9
if ((c >= 43 && c < 58) || c == "e".code || c == "E".code)
pos++;
else
break;
}
return Std.parseFloat(buf.slice(p1, pos).join(""));
}
function unserializeObject(o:{}) {
while (true) {
if (pos >= length)
throw "Invalid object";
if (get(pos) == "g".code)
break;
var k:Dynamic = unserialize();
if (!Std.is(k, String))
throw "Invalid object key";
var v = unserialize();
Reflect.setField(o, k, v);
}
pos++;
}
function unserializeEnum<T>(edecl:Enum<T>, tag:String) {
if (get(pos++) != ":".code)
throw "Invalid enum format";
var nargs = readDigits();
if (nargs == 0)
return Type.createEnum(edecl, tag);
var args = new Array();
while (nargs-- > 0)
args.push(unserialize());
return Type.createEnum(edecl, tag, args);
}
/**
Unserializes the next part of `this` Unserializer instance and returns
the according value.
This function may call `this.resolver.resolveClass` to determine a
Class from a String, and `this.resolver.resolveEnum` to determine an
Enum from a String.
If `this` Unserializer instance contains no more or invalid data, an
exception is thrown.
This operation may fail on structurally valid data if a type cannot be
resolved or if a field cannot be set. This can happen when unserializing
Strings that were serialized on a different Haxe target, in which the
serialization side has to make sure not to include platform-specific
data.
Classes are created from `Type.createEmptyInstance`, which means their
constructors are not called.
**/
public function unserialize():Dynamic {
switch (get(pos++)) {
case "n".code:
return null;
case "t".code:
return true;
case "f".code:
return false;
case "z".code:
return 0;
case "i".code:
return readDigits();
case "d".code:
return readFloat();
case "y".code:
var len = readDigits();
if (get(pos++) != ":".code || length - pos < len)
throw "Invalid string length";
var s = buf.slice(pos, pos+len).join("");
pos += len;
s = StringTools.urlDecode(s);
scache.push(s);
return s;
case "k".code:
return Math.NaN;
case "m".code:
return Math.NEGATIVE_INFINITY;
case "p".code:
return Math.POSITIVE_INFINITY;
case "a".code:
var buf = buf;
var a = new Array<Dynamic>();
#if cpp
var cachePos = cache.length;
#end
cache.push(a);
while (true) {
var c = get(pos);
if (c == "h".code) {
pos++;
break;
}
if (c == "u".code) {
pos++;
var n = readDigits();
a[a.length + n - 1] = null;
} else
a.push(unserialize());
}
#if cpp
return cache[cachePos] = cpp.NativeArray.resolveVirtualArray(a);
#else
return a;
#end
case "o".code:
var o = {};
cache.push(o);
unserializeObject(o);
return o;
case "r".code:
var n = readDigits();
if (n < 0 || n >= cache.length)
throw "Invalid reference";
return cache[n];
case "R".code:
var n = readDigits();
if (n < 0 || n >= scache.length)
throw "Invalid string reference";
return scache[n];
case "x".code:
throw unserialize();
case "c".code:
var name = unserialize();
var cl = resolver.resolveClass(name);
if (cl == null)
throw "Class not found " + name;
var o = Type.createEmptyInstance(cl);
cache.push(o);
unserializeObject(o);
return o;
case "w".code:
var name = unserialize();
var edecl = resolver.resolveEnum(name);
if (edecl == null)
throw "Enum not found " + name;
var e = unserializeEnum(edecl, unserialize());
cache.push(e);
return e;
case "j".code:
var name = unserialize();
var edecl = resolver.resolveEnum(name);
if (edecl == null)
throw "Enum not found " + name;
pos++; /* skip ':' */
var index = readDigits();
var tag = Type.getEnumConstructs(edecl)[index];
if (tag == null)
throw "Unknown enum index " + name + "@" + index;
var e = unserializeEnum(edecl, tag);
cache.push(e);
return e;
case "l".code:
var l = new List();
cache.push(l);
var buf = buf;
while (get(pos) != "h".code)
l.add(unserialize());
pos++;
return l;
case "b".code:
var h = new haxe.ds.StringMap();
cache.push(h);
var buf = buf;
while (get(pos) != "h".code) {
var s = unserialize();
h.set(s, unserialize());
}
pos++;
return h;
case "q".code:
var h = new haxe.ds.IntMap();
cache.push(h);
var buf = buf;
var c = get(pos++);
while (c == ":".code) {
var i = readDigits();
h.set(i, unserialize());
c = get(pos++);
}
if (c != "h".code)
throw "Invalid IntMap format";
return h;
case "M".code:
var h = new haxe.ds.ObjectMap();
cache.push(h);
var buf = buf;
while (get(pos) != "h".code) {
var s = unserialize();
h.set(s, unserialize());
}
pos++;
return h;
case "v".code:
var d;
if (get(pos) >= '0'.code && get(pos) <= '9'.code && get(pos + 1) >= '0'.code && get(pos + 1) <= '9'.code && get(pos + 2) >= '0'.code
&& get(pos + 2) <= '9'.code && get(pos + 3) >= '0'.code && get(pos + 3) <= '9'.code && get(pos + 4) == '-'.code) {
// Included for backwards compatibility
d = Date.fromString(buf.slice(pos, pos+19).join(""));
pos += 19;
} else
d = Date.fromTime(readFloat());
cache.push(d);
return d;
case "s".code:
var len = readDigits();
if (get(pos++) != ":".code || length - pos < len)
throw "Invalid bytes length";
#if neko
var bytes = haxe.io.Bytes.ofData(base_decode(untyped buf.substr(pos, len).__s, untyped BASE64.__s));
#else
var codes = CODES;
if (codes == null) {
codes = initCodes();
CODES = codes;
}
var i = pos;
var rest = len & 3;
var size = (len >> 2) * 3 + ((rest >= 2) ? rest - 1 : 0);
var max = i + (len - rest);
var bytes = haxe.io.Bytes.alloc(size);
var bpos = 0;
while (i < max) {
var c1 = codes[get(i++)];
var c2 = codes[get(i++)];
bytes.set(bpos++, (c1 << 2) | (c2 >> 4));
var c3 = codes[get(i++)];
bytes.set(bpos++, (c2 << 4) | (c3 >> 2));
var c4 = codes[get(i++)];
var prevTime = Date.now().getTime();
bytes.set(bpos++, (c3 << 6) | c4);
}
if (rest >= 2) {
var c1 = codes[get(i++)];
var c2 = codes[get(i++)];
bytes.set(bpos++, (c1 << 2) | (c2 >> 4));
if (rest == 3) {
var c3 = codes[get(i++)];
bytes.set(bpos++, (c2 << 4) | (c3 >> 2));
}
}
#end
pos += len;
cache.push(bytes);
return bytes;
case "C".code:
var name = unserialize();
var cl = resolver.resolveClass(name);
if (cl == null)
throw "Class not found " + name;
var o:Dynamic = Type.createEmptyInstance(cl);
cache.push(o);
o.hxUnserialize(this);
if (get(pos++) != "g".code)
throw "Invalid custom data";
return o;
case "A".code:
var name = unserialize();
var cl = resolver.resolveClass(name);
if (cl == null)
throw "Class not found " + name;
return cl;
case "B".code:
var name = unserialize();
var e = resolver.resolveEnum(name);
if (e == null)
throw "Enum not found " + name;
return e;
default:
}
pos--;
throw("Invalid char " + buf[pos] + " at position " + pos);
}
/**
Unserializes `v` and returns the according value.
This is a convenience function for creating a new instance of
Unserializer with `v` as buffer and calling its `unserialize()` method
once.
**/
public static function run(v:String):Dynamic {
return new Unserializer(v).unserialize();
}
#if neko
static var base_decode = neko.Lib.load("std", "base_decode", 2);
#end
}
private class DefaultResolver {
public function new() {}
public inline function resolveClass(name:String):Class<Dynamic>
return Type.resolveClass(name);
public inline function resolveEnum(name:String):Enum<Dynamic>
return Type.resolveEnum(name);
}
private class NullResolver {
function new() {}
public inline function resolveClass(name:String):Class<Dynamic>
return null;
public inline function resolveEnum(name:String):Enum<Dynamic>
return null;
public static var instance(get, null):NullResolver;
inline static function get_instance():NullResolver {
if (instance == null)
instance = new NullResolver();
return instance;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment