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