/* * 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; using haxe.Unserializer; import haxe.ds.List; @:noDoc typedef TypeResolver = { function resolveClass(name:String):Class; function resolveEnum(name:String):Enum; } /** 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: **/ 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` is called to determine a `Class` from a class name 2. `resolveEnum(name:String):Enum` 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:String; var pos:Int; var length:Int; var cache:Array; var scache:Array; 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; length = this.buf.fastLength(); 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):Int { #if php return p >= length ? 0 : buf.fastCharCodeAt(p); #else return StringTools.fastCodeAt(buf, p); #end } 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.fastSubstr(p1, pos - p1)); } function unserializeObject(o:{}) { while (true) { if (pos >= length) throw "Invalid object"; if (get(pos) == "g".code) break; var k:Dynamic = unserialize(); if (!Std.isOfType(k, String)) throw "Invalid object key"; var v = unserialize(); Reflect.setField(o, k, v); } pos++; } function unserializeEnum(edecl:Enum, 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.fastSubstr(pos, len); 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(); #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.fastSubstr(pos, 19)); pos += 19; } else d = Date.fromTime(readFloat()); cache.push(d); return d; case "s".code: var len = readDigits(); var buf = buf; if (get(pos++) != ":".code || length - pos < len) throw "Invalid bytes length"; #if neko var bytes = haxe.io.Bytes.ofData(base_decode(untyped buf.fastSubstr(pos, len).__s, untyped BASE64.__s)); #elseif php var phpEncoded = php.Global.strtr(buf.fastSubstr(pos, len), '%:', '+/'); var bytes = haxe.io.Bytes.ofData(php.Global.base64_decode(phpEncoded)); #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[StringTools.fastCodeAt(buf, i++)]; var c2 = codes[StringTools.fastCodeAt(buf, i++)]; bytes.set(bpos++, (c1 << 2) | (c2 >> 4)); var c3 = codes[StringTools.fastCodeAt(buf, i++)]; bytes.set(bpos++, (c2 << 4) | (c3 >> 2)); var c4 = codes[StringTools.fastCodeAt(buf, i++)]; bytes.set(bpos++, (c3 << 6) | c4); } if (rest >= 2) { var c1 = codes[StringTools.fastCodeAt(buf, i++)]; var c2 = codes[StringTools.fastCodeAt(buf, i++)]; bytes.set(bpos++, (c1 << 2) | (c2 >> 4)); if (rest == 3) { var c3 = codes[StringTools.fastCodeAt(buf, 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.fastCharAt(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 static inline function fastLength(s:String):Int { #if php return php.Global.strlen(s); #else return s.length; #end } static inline function fastCharCodeAt(s:String, pos:Int):Int { #if php return php.Global.ord((s:php.NativeString)[pos]); #else return s.charCodeAt(pos); #end } static inline function fastCharAt(s:String, pos:Int):String { #if php return (s:php.NativeString)[pos]; #else return s.charAt(pos); #end } static inline function fastSubstr(s:String, pos:Int, length:Int):String { #if php return php.Global.substr(s, pos, length); #else return s.substr(pos, length); #end } } private class DefaultResolver { public function new() {} public inline function resolveClass(name:String):Class return Type.resolveClass(name); public inline function resolveEnum(name:String):Enum return Type.resolveEnum(name); } private class NullResolver { function new() {} public inline function resolveClass(name:String):Class return null; public inline function resolveEnum(name:String):Enum return null; public static var instance(get, null):NullResolver; inline static function get_instance():NullResolver { if (instance == null) instance = new NullResolver(); return instance; } }