yellowfive@57: --- **AceSerializer-3.0** can serialize any variable (except functions or userdata) into a string format, yellowfive@57: -- that can be send over the addon comm channel. AceSerializer was designed to keep all data intact, especially yellowfive@57: -- very large numbers or floating point numbers, and table structures. The only caveat currently is, that multiple yellowfive@57: -- references to the same table will be send individually. yellowfive@57: -- yellowfive@57: -- **AceSerializer-3.0** can be embeded into your addon, either explicitly by calling AceSerializer:Embed(MyAddon) or by yellowfive@57: -- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object yellowfive@57: -- and can be accessed directly, without having to explicitly call AceSerializer itself.\\ yellowfive@57: -- It is recommended to embed AceSerializer, otherwise you'll have to specify a custom `self` on all calls you yellowfive@57: -- make into AceSerializer. yellowfive@57: -- @class file yellowfive@57: -- @name AceSerializer-3.0 yellowfive@106: -- @release $Id: AceSerializer-3.0.lua 1135 2015-09-19 20:39:16Z nevcairiel $ yellowfive@106: local MAJOR,MINOR = "AceSerializer-3.0", 5 yellowfive@57: local AceSerializer, oldminor = LibStub:NewLibrary(MAJOR, MINOR) yellowfive@57: yellowfive@57: if not AceSerializer then return end yellowfive@57: yellowfive@57: -- Lua APIs yellowfive@57: local strbyte, strchar, gsub, gmatch, format = string.byte, string.char, string.gsub, string.gmatch, string.format yellowfive@57: local assert, error, pcall = assert, error, pcall yellowfive@57: local type, tostring, tonumber = type, tostring, tonumber yellowfive@57: local pairs, select, frexp = pairs, select, math.frexp yellowfive@57: local tconcat = table.concat yellowfive@57: yellowfive@57: -- quick copies of string representations of wonky numbers yellowfive@57: local inf = math.huge yellowfive@57: yellowfive@57: local serNaN -- can't do this in 4.3, see ace3 ticket 268 yellowfive@106: local serInf, serInfMac = "1.#INF", "inf" yellowfive@106: local serNegInf, serNegInfMac = "-1.#INF", "-inf" yellowfive@57: yellowfive@57: yellowfive@57: -- Serialization functions yellowfive@57: yellowfive@57: local function SerializeStringHelper(ch) -- Used by SerializeValue for strings yellowfive@57: -- We use \126 ("~") as an escape character for all nonprints plus a few more yellowfive@57: local n = strbyte(ch) yellowfive@57: if n==30 then -- v3 / ticket 115: catch a nonprint that ends up being "~^" when encoded... DOH yellowfive@57: return "\126\122" yellowfive@57: elseif n<=32 then -- nonprint + space yellowfive@57: return "\126"..strchar(n+64) yellowfive@57: elseif n==94 then -- value separator yellowfive@57: return "\126\125" yellowfive@57: elseif n==126 then -- our own escape character yellowfive@57: return "\126\124" yellowfive@57: elseif n==127 then -- nonprint (DEL) yellowfive@57: return "\126\123" yellowfive@57: else yellowfive@57: assert(false) -- can't be reached if caller uses a sane regex yellowfive@57: end yellowfive@57: end yellowfive@57: yellowfive@57: local function SerializeValue(v, res, nres) yellowfive@57: -- We use "^" as a value separator, followed by one byte for type indicator yellowfive@57: local t=type(v) yellowfive@57: yellowfive@57: if t=="string" then -- ^S = string (escaped to remove nonprints, "^"s, etc) yellowfive@57: res[nres+1] = "^S" yellowfive@57: res[nres+2] = gsub(v,"[%c \94\126\127]", SerializeStringHelper) yellowfive@57: nres=nres+2 yellowfive@57: yellowfive@57: elseif t=="number" then -- ^N = number (just tostring()ed) or ^F (float components) yellowfive@57: local str = tostring(v) yellowfive@106: if tonumber(str)==v --[[not in 4.3 or str==serNaN]] then yellowfive@57: -- translates just fine, transmit as-is yellowfive@57: res[nres+1] = "^N" yellowfive@57: res[nres+2] = str yellowfive@57: nres=nres+2 yellowfive@106: elseif v == inf or v == -inf then yellowfive@106: res[nres+1] = "^N" yellowfive@106: res[nres+2] = v == inf and serInf or serNegInf yellowfive@106: nres=nres+2 yellowfive@57: else yellowfive@57: local m,e = frexp(v) yellowfive@57: res[nres+1] = "^F" yellowfive@57: res[nres+2] = format("%.0f",m*2^53) -- force mantissa to become integer (it's originally 0.5--0.9999) yellowfive@57: res[nres+3] = "^f" yellowfive@57: res[nres+4] = tostring(e-53) -- adjust exponent to counteract mantissa manipulation yellowfive@57: nres=nres+4 yellowfive@57: end yellowfive@57: yellowfive@57: elseif t=="table" then -- ^T...^t = table (list of key,value pairs) yellowfive@57: nres=nres+1 yellowfive@57: res[nres] = "^T" yellowfive@57: for k,v in pairs(v) do yellowfive@57: nres = SerializeValue(k, res, nres) yellowfive@57: nres = SerializeValue(v, res, nres) yellowfive@57: end yellowfive@57: nres=nres+1 yellowfive@57: res[nres] = "^t" yellowfive@57: yellowfive@57: elseif t=="boolean" then -- ^B = true, ^b = false yellowfive@57: nres=nres+1 yellowfive@57: if v then yellowfive@57: res[nres] = "^B" -- true yellowfive@57: else yellowfive@57: res[nres] = "^b" -- false yellowfive@57: end yellowfive@57: yellowfive@57: elseif t=="nil" then -- ^Z = nil (zero, "N" was taken :P) yellowfive@57: nres=nres+1 yellowfive@57: res[nres] = "^Z" yellowfive@57: yellowfive@57: else yellowfive@57: error(MAJOR..": Cannot serialize a value of type '"..t.."'") -- can't produce error on right level, this is wildly recursive yellowfive@57: end yellowfive@57: yellowfive@57: return nres yellowfive@57: end yellowfive@57: yellowfive@57: yellowfive@57: yellowfive@57: local serializeTbl = { "^1" } -- "^1" = Hi, I'm data serialized by AceSerializer protocol rev 1 yellowfive@57: yellowfive@57: --- Serialize the data passed into the function. yellowfive@57: -- Takes a list of values (strings, numbers, booleans, nils, tables) yellowfive@57: -- and returns it in serialized form (a string).\\ yellowfive@57: -- May throw errors on invalid data types. yellowfive@57: -- @param ... List of values to serialize yellowfive@57: -- @return The data in its serialized form (string) yellowfive@57: function AceSerializer:Serialize(...) yellowfive@57: local nres = 1 yellowfive@57: yellowfive@57: for i=1,select("#", ...) do yellowfive@57: local v = select(i, ...) yellowfive@57: nres = SerializeValue(v, serializeTbl, nres) yellowfive@57: end yellowfive@57: yellowfive@57: serializeTbl[nres+1] = "^^" -- "^^" = End of serialized data yellowfive@57: yellowfive@57: return tconcat(serializeTbl, "", 1, nres+1) yellowfive@57: end yellowfive@57: yellowfive@57: -- Deserialization functions yellowfive@57: local function DeserializeStringHelper(escape) yellowfive@57: if escape<"~\122" then yellowfive@57: return strchar(strbyte(escape,2,2)-64) yellowfive@57: elseif escape=="~\122" then -- v3 / ticket 115: special case encode since 30+64=94 ("^") - OOPS. yellowfive@57: return "\030" yellowfive@57: elseif escape=="~\123" then yellowfive@57: return "\127" yellowfive@57: elseif escape=="~\124" then yellowfive@57: return "\126" yellowfive@57: elseif escape=="~\125" then yellowfive@57: return "\94" yellowfive@57: end yellowfive@57: error("DeserializeStringHelper got called for '"..escape.."'?!?") -- can't be reached unless regex is screwed up yellowfive@57: end yellowfive@57: yellowfive@57: local function DeserializeNumberHelper(number) yellowfive@57: --[[ not in 4.3 if number == serNaN then yellowfive@57: return 0/0 yellowfive@106: else]]if number == serNegInf or number == serNegInfMac then yellowfive@57: return -inf yellowfive@106: elseif number == serInf or number == serInfMac then yellowfive@57: return inf yellowfive@57: else yellowfive@57: return tonumber(number) yellowfive@57: end yellowfive@57: end yellowfive@57: yellowfive@57: -- DeserializeValue: worker function for :Deserialize() yellowfive@57: -- It works in two modes: yellowfive@57: -- Main (top-level) mode: Deserialize a list of values and return them all yellowfive@57: -- Recursive (table) mode: Deserialize only a single value (_may_ of course be another table with lots of subvalues in it) yellowfive@57: -- yellowfive@57: -- The function _always_ works recursively due to having to build a list of values to return yellowfive@57: -- yellowfive@57: -- Callers are expected to pcall(DeserializeValue) to trap errors yellowfive@57: yellowfive@57: local function DeserializeValue(iter,single,ctl,data) yellowfive@57: yellowfive@57: if not single then yellowfive@57: ctl,data = iter() yellowfive@57: end yellowfive@57: yellowfive@57: if not ctl then yellowfive@57: error("Supplied data misses AceSerializer terminator ('^^')") yellowfive@57: end yellowfive@57: yellowfive@57: if ctl=="^^" then yellowfive@57: -- ignore extraneous data yellowfive@57: return yellowfive@57: end yellowfive@57: yellowfive@57: local res yellowfive@57: yellowfive@57: if ctl=="^S" then yellowfive@57: res = gsub(data, "~.", DeserializeStringHelper) yellowfive@57: elseif ctl=="^N" then yellowfive@57: res = DeserializeNumberHelper(data) yellowfive@57: if not res then yellowfive@57: error("Invalid serialized number: '"..tostring(data).."'") yellowfive@57: end yellowfive@57: elseif ctl=="^F" then -- ^F^f yellowfive@57: local ctl2,e = iter() yellowfive@57: if ctl2~="^f" then yellowfive@57: error("Invalid serialized floating-point number, expected '^f', not '"..tostring(ctl2).."'") yellowfive@57: end yellowfive@57: local m=tonumber(data) yellowfive@57: e=tonumber(e) yellowfive@57: if not (m and e) then yellowfive@57: error("Invalid serialized floating-point number, expected mantissa and exponent, got '"..tostring(m).."' and '"..tostring(e).."'") yellowfive@57: end yellowfive@57: res = m*(2^e) yellowfive@57: elseif ctl=="^B" then -- yeah yeah ignore data portion yellowfive@57: res = true yellowfive@57: elseif ctl=="^b" then -- yeah yeah ignore data portion yellowfive@57: res = false yellowfive@57: elseif ctl=="^Z" then -- yeah yeah ignore data portion yellowfive@57: res = nil yellowfive@57: elseif ctl=="^T" then yellowfive@57: -- ignore ^T's data, future extensibility? yellowfive@57: res = {} yellowfive@57: local k,v yellowfive@57: while true do yellowfive@57: ctl,data = iter() yellowfive@57: if ctl=="^t" then break end -- ignore ^t's data yellowfive@57: k = DeserializeValue(iter,true,ctl,data) yellowfive@57: if k==nil then yellowfive@57: error("Invalid AceSerializer table format (no table end marker)") yellowfive@57: end yellowfive@57: ctl,data = iter() yellowfive@57: v = DeserializeValue(iter,true,ctl,data) yellowfive@57: if v==nil then yellowfive@57: error("Invalid AceSerializer table format (no table end marker)") yellowfive@57: end yellowfive@57: res[k]=v yellowfive@57: end yellowfive@57: else yellowfive@57: error("Invalid AceSerializer control code '"..ctl.."'") yellowfive@57: end yellowfive@57: yellowfive@57: if not single then yellowfive@57: return res,DeserializeValue(iter) yellowfive@57: else yellowfive@57: return res yellowfive@57: end yellowfive@57: end yellowfive@57: yellowfive@57: --- Deserializes the data into its original values. yellowfive@57: -- Accepts serialized data, ignoring all control characters and whitespace. yellowfive@57: -- @param str The serialized data (from :Serialize) yellowfive@57: -- @return true followed by a list of values, OR false followed by an error message yellowfive@57: function AceSerializer:Deserialize(str) yellowfive@57: str = gsub(str, "[%c ]", "") -- ignore all control characters; nice for embedding in email and stuff yellowfive@57: yellowfive@57: local iter = gmatch(str, "(^.)([^^]*)") -- Any ^x followed by string of non-^ yellowfive@57: local ctl,data = iter() yellowfive@57: if not ctl or ctl~="^1" then yellowfive@57: -- we purposefully ignore the data portion of the start code, it can be used as an extension mechanism yellowfive@57: return false, "Supplied data is not AceSerializer data (rev 1)" yellowfive@57: end yellowfive@57: yellowfive@57: return pcall(DeserializeValue, iter) yellowfive@57: end yellowfive@57: yellowfive@57: yellowfive@57: ---------------------------------------- yellowfive@57: -- Base library stuff yellowfive@57: ---------------------------------------- yellowfive@57: yellowfive@57: AceSerializer.internals = { -- for test scripts yellowfive@57: SerializeValue = SerializeValue, yellowfive@57: SerializeStringHelper = SerializeStringHelper, yellowfive@57: } yellowfive@57: yellowfive@57: local mixins = { yellowfive@57: "Serialize", yellowfive@57: "Deserialize", yellowfive@57: } yellowfive@57: yellowfive@57: AceSerializer.embeds = AceSerializer.embeds or {} yellowfive@57: yellowfive@57: function AceSerializer:Embed(target) yellowfive@57: for k, v in pairs(mixins) do yellowfive@57: target[v] = self[v] yellowfive@57: end yellowfive@57: self.embeds[target] = true yellowfive@57: return target yellowfive@57: end yellowfive@57: yellowfive@57: -- Update embeds yellowfive@57: for target, v in pairs(AceSerializer.embeds) do yellowfive@57: AceSerializer:Embed(target) yellowfive@106: end