WoWInterface SVN RaidWatch2

[/] [trunk/] [RaidWatch/] [Libs/] [AceSerializer-3.0/] [AceSerializer-3.0.lua] - Rev 22

Compare with Previous | Blame | View Log

--- **AceSerializer-3.0** can serialize any variable (except functions or userdata) into a string format,
-- that can be send over the addon comm channel. AceSerializer was designed to keep all data intact, especially 
-- very large numbers or floating point numbers, and table structures. The only caveat currently is, that multiple
-- references to the same table will be send individually.
--
-- **AceSerializer-3.0** can be embeded into your addon, either explicitly by calling AceSerializer:Embed(MyAddon) or by 
-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
-- and can be accessed directly, without having to explicitly call AceSerializer itself.\\
-- It is recommended to embed AceSerializer, otherwise you'll have to specify a custom `self` on all calls you
-- make into AceSerializer.
-- @class file
-- @name AceSerializer-3.0
-- @release $Id: AceSerializer-3.0.lua 910 2010-02-11 21:54:24Z mikk $
local MAJOR,MINOR = "AceSerializer-3.0", 3
local AceSerializer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)

if not AceSerializer then return end

-- Lua APIs
local strbyte, strchar, gsub, gmatch, format = string.byte, string.char, string.gsub, string.gmatch, string.format
local assert, error, pcall = assert, error, pcall
local type, tostring, tonumber = type, tostring, tonumber
local pairs, select, frexp = pairs, select, math.frexp
local tconcat = table.concat

-- quick copies of string representations of wonky numbers
local serNaN = tostring(0/0)
local serInf = tostring(1/0)
local serNegInf = tostring(-1/0)


-- Serialization functions

local function SerializeStringHelper(ch)        -- Used by SerializeValue for strings
        -- We use \126 ("~") as an escape character for all nonprints plus a few more
        local n = strbyte(ch)
        if n==30 then           -- v3 / ticket 115: catch a nonprint that ends up being "~^" when encoded... DOH
                return "\126\122"
        elseif n<=32 then                       -- nonprint + space
                return "\126"..strchar(n+64)
        elseif n==94 then               -- value separator 
                return "\126\125"
        elseif n==126 then              -- our own escape character
                return "\126\124"
        elseif n==127 then              -- nonprint (DEL)
                return "\126\123"
        else
                assert(false)   -- can't be reached if caller uses a sane regex
        end
end

local function SerializeValue(v, res, nres)
        -- We use "^" as a value separator, followed by one byte for type indicator
        local t=type(v)
        
        if t=="string" then             -- ^S = string (escaped to remove nonprints, "^"s, etc)
                res[nres+1] = "^S"
                res[nres+2] = gsub(v,"[%c \94\126\127]", SerializeStringHelper)
                nres=nres+2
        
        elseif t=="number" then -- ^N = number (just tostring()ed) or ^F (float components)
                local str = tostring(v)
                if tonumber(str)==v  or str==serNaN or str==serInf or str==serNegInf then
                        -- translates just fine, transmit as-is
                        res[nres+1] = "^N"
                        res[nres+2] = str
                        nres=nres+2
                else
                        local m,e = frexp(v)
                        res[nres+1] = "^F"
                        res[nres+2] = format("%.0f",m*2^53)     -- force mantissa to become integer (it's originally 0.5--0.9999)
                        res[nres+3] = "^f"
                        res[nres+4] = tostring(e-53)    -- adjust exponent to counteract mantissa manipulation
                        nres=nres+4
                end
        
        elseif t=="table" then  -- ^T...^t = table (list of key,value pairs)
                nres=nres+1
                res[nres] = "^T"
                for k,v in pairs(v) do
                        nres = SerializeValue(k, res, nres)
                        nres = SerializeValue(v, res, nres)
                end
                nres=nres+1
                res[nres] = "^t"
        
        elseif t=="boolean" then        -- ^B = true, ^b = false
                nres=nres+1
                if v then
                        res[nres] = "^B"        -- true
                else
                        res[nres] = "^b"        -- false
                end
        
        elseif t=="nil" then            -- ^Z = nil (zero, "N" was taken :P)
                nres=nres+1
                res[nres] = "^Z"
        
        else
                error(MAJOR..": Cannot serialize a value of type '"..t.."'")    -- can't produce error on right level, this is wildly recursive
        end
        
        return nres
end



local serializeTbl = { "^1" }   -- "^1" = Hi, I'm data serialized by AceSerializer protocol rev 1

--- Serialize the data passed into the function.
-- Takes a list of values (strings, numbers, booleans, nils, tables)
-- and returns it in serialized form (a string).\\
-- May throw errors on invalid data types.
-- @param ... List of values to serialize
-- @return The data in its serialized form (string)
function AceSerializer:Serialize(...)
        local nres = 1
        
        for i=1,select("#", ...) do
                local v = select(i, ...)
                nres = SerializeValue(v, serializeTbl, nres)
        end
        
        serializeTbl[nres+1] = "^^"     -- "^^" = End of serialized data
        
        return tconcat(serializeTbl, "", 1, nres+1)
end

-- Deserialization functions
local function DeserializeStringHelper(escape)
        if escape<"~\122" then
                return strchar(strbyte(escape,2,2)-64)
        elseif escape=="~\122" then     -- v3 / ticket 115: special case encode since 30+64=94 ("^") - OOPS.
                return "\030"
        elseif escape=="~\123" then
                return "\127"
        elseif escape=="~\124" then
                return "\126"
        elseif escape=="~\125" then
                return "\94"
        end
        error("DeserializeStringHelper got called for '"..escape.."'?!?")  -- can't be reached unless regex is screwed up
end

local function DeserializeNumberHelper(number)
        if number == serNaN then
                return 0/0
        elseif number == serNegInf then
                return -1/0
        elseif number == serInf then
                return 1/0
        else
                return tonumber(number)
        end
end

-- DeserializeValue: worker function for :Deserialize()
-- It works in two modes:
--   Main (top-level) mode: Deserialize a list of values and return them all
--   Recursive (table) mode: Deserialize only a single value (_may_ of course be another table with lots of subvalues in it)
--
-- The function _always_ works recursively due to having to build a list of values to return
--
-- Callers are expected to pcall(DeserializeValue) to trap errors

local function DeserializeValue(iter,single,ctl,data)

        if not single then
                ctl,data = iter()
        end

        if not ctl then 
                error("Supplied data misses AceSerializer terminator ('^^')")
        end     

        if ctl=="^^" then
                -- ignore extraneous data
                return
        end

        local res
        
        if ctl=="^S" then
                res = gsub(data, "~.", DeserializeStringHelper)
        elseif ctl=="^N" then
                res = DeserializeNumberHelper(data)
                if not res then
                        error("Invalid serialized number: '"..tostring(data).."'")
                end
        elseif ctl=="^F" then     -- ^F<mantissa>^f<exponent>
                local ctl2,e = iter()
                if ctl2~="^f" then
                        error("Invalid serialized floating-point number, expected '^f', not '"..tostring(ctl2).."'")
                end
                local m=tonumber(data)
                e=tonumber(e)
                if not (m and e) then
                        error("Invalid serialized floating-point number, expected mantissa and exponent, got '"..tostring(m).."' and '"..tostring(e).."'")
                end
                res = m*(2^e)
        elseif ctl=="^B" then   -- yeah yeah ignore data portion
                res = true
        elseif ctl=="^b" then   -- yeah yeah ignore data portion
                res = false
        elseif ctl=="^Z" then   -- yeah yeah ignore data portion
                res = nil
        elseif ctl=="^T" then
                -- ignore ^T's data, future extensibility?
                res = {}
                local k,v
                while true do
                        ctl,data = iter()
                        if ctl=="^t" then break end     -- ignore ^t's data
                        k = DeserializeValue(iter,true,ctl,data)
                        if k==nil then 
                                error("Invalid AceSerializer table format (no table end marker)")
                        end
                        ctl,data = iter()
                        v = DeserializeValue(iter,true,ctl,data)
                        if v==nil then
                                error("Invalid AceSerializer table format (no table end marker)")
                        end
                        res[k]=v
                end
        else
                error("Invalid AceSerializer control code '"..ctl.."'")
        end
        
        if not single then
                return res,DeserializeValue(iter)
        else
                return res
        end
end

--- Deserializes the data into its original values.
-- Accepts serialized data, ignoring all control characters and whitespace.
-- @param str The serialized data (from :Serialize)
-- @return true followed by a list of values, OR false followed by an error message
function AceSerializer:Deserialize(str)
        str = gsub(str, "[%c ]", "")    -- ignore all control characters; nice for embedding in email and stuff

        local iter = gmatch(str, "(^.)([^^]*)") -- Any ^x followed by string of non-^
        local ctl,data = iter()
        if not ctl or ctl~="^1" then
                -- we purposefully ignore the data portion of the start code, it can be used as an extension mechanism
                return false, "Supplied data is not AceSerializer data (rev 1)"
        end

        return pcall(DeserializeValue, iter)
end


----------------------------------------
-- Base library stuff
----------------------------------------

AceSerializer.internals = {     -- for test scripts
        SerializeValue = SerializeValue,
        SerializeStringHelper = SerializeStringHelper,
}

local mixins = {
        "Serialize",
        "Deserialize",
}

AceSerializer.embeds = AceSerializer.embeds or {}

function AceSerializer:Embed(target)
        for k, v in pairs(mixins) do
                target[v] = self[v]
        end
        self.embeds[target] = true
        return target
end

-- Update embeds
for target, v in pairs(AceSerializer.embeds) do
        AceSerializer:Embed(target)
end

Compare with Previous | Blame