yellowfive@57
|
1 --- **AceSerializer-3.0** can serialize any variable (except functions or userdata) into a string format,
|
yellowfive@57
|
2 -- that can be send over the addon comm channel. AceSerializer was designed to keep all data intact, especially
|
yellowfive@57
|
3 -- very large numbers or floating point numbers, and table structures. The only caveat currently is, that multiple
|
yellowfive@57
|
4 -- references to the same table will be send individually.
|
yellowfive@57
|
5 --
|
yellowfive@57
|
6 -- **AceSerializer-3.0** can be embeded into your addon, either explicitly by calling AceSerializer:Embed(MyAddon) or by
|
yellowfive@57
|
7 -- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
|
yellowfive@57
|
8 -- and can be accessed directly, without having to explicitly call AceSerializer itself.\\
|
yellowfive@57
|
9 -- It is recommended to embed AceSerializer, otherwise you'll have to specify a custom `self` on all calls you
|
yellowfive@57
|
10 -- make into AceSerializer.
|
yellowfive@57
|
11 -- @class file
|
yellowfive@57
|
12 -- @name AceSerializer-3.0
|
yellowfive@106
|
13 -- @release $Id: AceSerializer-3.0.lua 1135 2015-09-19 20:39:16Z nevcairiel $
|
yellowfive@106
|
14 local MAJOR,MINOR = "AceSerializer-3.0", 5
|
yellowfive@57
|
15 local AceSerializer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
|
yellowfive@57
|
16
|
yellowfive@57
|
17 if not AceSerializer then return end
|
yellowfive@57
|
18
|
yellowfive@57
|
19 -- Lua APIs
|
yellowfive@57
|
20 local strbyte, strchar, gsub, gmatch, format = string.byte, string.char, string.gsub, string.gmatch, string.format
|
yellowfive@57
|
21 local assert, error, pcall = assert, error, pcall
|
yellowfive@57
|
22 local type, tostring, tonumber = type, tostring, tonumber
|
yellowfive@57
|
23 local pairs, select, frexp = pairs, select, math.frexp
|
yellowfive@57
|
24 local tconcat = table.concat
|
yellowfive@57
|
25
|
yellowfive@57
|
26 -- quick copies of string representations of wonky numbers
|
yellowfive@57
|
27 local inf = math.huge
|
yellowfive@57
|
28
|
yellowfive@57
|
29 local serNaN -- can't do this in 4.3, see ace3 ticket 268
|
yellowfive@106
|
30 local serInf, serInfMac = "1.#INF", "inf"
|
yellowfive@106
|
31 local serNegInf, serNegInfMac = "-1.#INF", "-inf"
|
yellowfive@57
|
32
|
yellowfive@57
|
33
|
yellowfive@57
|
34 -- Serialization functions
|
yellowfive@57
|
35
|
yellowfive@57
|
36 local function SerializeStringHelper(ch) -- Used by SerializeValue for strings
|
yellowfive@57
|
37 -- We use \126 ("~") as an escape character for all nonprints plus a few more
|
yellowfive@57
|
38 local n = strbyte(ch)
|
yellowfive@57
|
39 if n==30 then -- v3 / ticket 115: catch a nonprint that ends up being "~^" when encoded... DOH
|
yellowfive@57
|
40 return "\126\122"
|
yellowfive@57
|
41 elseif n<=32 then -- nonprint + space
|
yellowfive@57
|
42 return "\126"..strchar(n+64)
|
yellowfive@57
|
43 elseif n==94 then -- value separator
|
yellowfive@57
|
44 return "\126\125"
|
yellowfive@57
|
45 elseif n==126 then -- our own escape character
|
yellowfive@57
|
46 return "\126\124"
|
yellowfive@57
|
47 elseif n==127 then -- nonprint (DEL)
|
yellowfive@57
|
48 return "\126\123"
|
yellowfive@57
|
49 else
|
yellowfive@57
|
50 assert(false) -- can't be reached if caller uses a sane regex
|
yellowfive@57
|
51 end
|
yellowfive@57
|
52 end
|
yellowfive@57
|
53
|
yellowfive@57
|
54 local function SerializeValue(v, res, nres)
|
yellowfive@57
|
55 -- We use "^" as a value separator, followed by one byte for type indicator
|
yellowfive@57
|
56 local t=type(v)
|
yellowfive@57
|
57
|
yellowfive@57
|
58 if t=="string" then -- ^S = string (escaped to remove nonprints, "^"s, etc)
|
yellowfive@57
|
59 res[nres+1] = "^S"
|
yellowfive@57
|
60 res[nres+2] = gsub(v,"[%c \94\126\127]", SerializeStringHelper)
|
yellowfive@57
|
61 nres=nres+2
|
yellowfive@57
|
62
|
yellowfive@57
|
63 elseif t=="number" then -- ^N = number (just tostring()ed) or ^F (float components)
|
yellowfive@57
|
64 local str = tostring(v)
|
yellowfive@106
|
65 if tonumber(str)==v --[[not in 4.3 or str==serNaN]] then
|
yellowfive@57
|
66 -- translates just fine, transmit as-is
|
yellowfive@57
|
67 res[nres+1] = "^N"
|
yellowfive@57
|
68 res[nres+2] = str
|
yellowfive@57
|
69 nres=nres+2
|
yellowfive@106
|
70 elseif v == inf or v == -inf then
|
yellowfive@106
|
71 res[nres+1] = "^N"
|
yellowfive@106
|
72 res[nres+2] = v == inf and serInf or serNegInf
|
yellowfive@106
|
73 nres=nres+2
|
yellowfive@57
|
74 else
|
yellowfive@57
|
75 local m,e = frexp(v)
|
yellowfive@57
|
76 res[nres+1] = "^F"
|
yellowfive@57
|
77 res[nres+2] = format("%.0f",m*2^53) -- force mantissa to become integer (it's originally 0.5--0.9999)
|
yellowfive@57
|
78 res[nres+3] = "^f"
|
yellowfive@57
|
79 res[nres+4] = tostring(e-53) -- adjust exponent to counteract mantissa manipulation
|
yellowfive@57
|
80 nres=nres+4
|
yellowfive@57
|
81 end
|
yellowfive@57
|
82
|
yellowfive@57
|
83 elseif t=="table" then -- ^T...^t = table (list of key,value pairs)
|
yellowfive@57
|
84 nres=nres+1
|
yellowfive@57
|
85 res[nres] = "^T"
|
yellowfive@57
|
86 for k,v in pairs(v) do
|
yellowfive@57
|
87 nres = SerializeValue(k, res, nres)
|
yellowfive@57
|
88 nres = SerializeValue(v, res, nres)
|
yellowfive@57
|
89 end
|
yellowfive@57
|
90 nres=nres+1
|
yellowfive@57
|
91 res[nres] = "^t"
|
yellowfive@57
|
92
|
yellowfive@57
|
93 elseif t=="boolean" then -- ^B = true, ^b = false
|
yellowfive@57
|
94 nres=nres+1
|
yellowfive@57
|
95 if v then
|
yellowfive@57
|
96 res[nres] = "^B" -- true
|
yellowfive@57
|
97 else
|
yellowfive@57
|
98 res[nres] = "^b" -- false
|
yellowfive@57
|
99 end
|
yellowfive@57
|
100
|
yellowfive@57
|
101 elseif t=="nil" then -- ^Z = nil (zero, "N" was taken :P)
|
yellowfive@57
|
102 nres=nres+1
|
yellowfive@57
|
103 res[nres] = "^Z"
|
yellowfive@57
|
104
|
yellowfive@57
|
105 else
|
yellowfive@57
|
106 error(MAJOR..": Cannot serialize a value of type '"..t.."'") -- can't produce error on right level, this is wildly recursive
|
yellowfive@57
|
107 end
|
yellowfive@57
|
108
|
yellowfive@57
|
109 return nres
|
yellowfive@57
|
110 end
|
yellowfive@57
|
111
|
yellowfive@57
|
112
|
yellowfive@57
|
113
|
yellowfive@57
|
114 local serializeTbl = { "^1" } -- "^1" = Hi, I'm data serialized by AceSerializer protocol rev 1
|
yellowfive@57
|
115
|
yellowfive@57
|
116 --- Serialize the data passed into the function.
|
yellowfive@57
|
117 -- Takes a list of values (strings, numbers, booleans, nils, tables)
|
yellowfive@57
|
118 -- and returns it in serialized form (a string).\\
|
yellowfive@57
|
119 -- May throw errors on invalid data types.
|
yellowfive@57
|
120 -- @param ... List of values to serialize
|
yellowfive@57
|
121 -- @return The data in its serialized form (string)
|
yellowfive@57
|
122 function AceSerializer:Serialize(...)
|
yellowfive@57
|
123 local nres = 1
|
yellowfive@57
|
124
|
yellowfive@57
|
125 for i=1,select("#", ...) do
|
yellowfive@57
|
126 local v = select(i, ...)
|
yellowfive@57
|
127 nres = SerializeValue(v, serializeTbl, nres)
|
yellowfive@57
|
128 end
|
yellowfive@57
|
129
|
yellowfive@57
|
130 serializeTbl[nres+1] = "^^" -- "^^" = End of serialized data
|
yellowfive@57
|
131
|
yellowfive@57
|
132 return tconcat(serializeTbl, "", 1, nres+1)
|
yellowfive@57
|
133 end
|
yellowfive@57
|
134
|
yellowfive@57
|
135 -- Deserialization functions
|
yellowfive@57
|
136 local function DeserializeStringHelper(escape)
|
yellowfive@57
|
137 if escape<"~\122" then
|
yellowfive@57
|
138 return strchar(strbyte(escape,2,2)-64)
|
yellowfive@57
|
139 elseif escape=="~\122" then -- v3 / ticket 115: special case encode since 30+64=94 ("^") - OOPS.
|
yellowfive@57
|
140 return "\030"
|
yellowfive@57
|
141 elseif escape=="~\123" then
|
yellowfive@57
|
142 return "\127"
|
yellowfive@57
|
143 elseif escape=="~\124" then
|
yellowfive@57
|
144 return "\126"
|
yellowfive@57
|
145 elseif escape=="~\125" then
|
yellowfive@57
|
146 return "\94"
|
yellowfive@57
|
147 end
|
yellowfive@57
|
148 error("DeserializeStringHelper got called for '"..escape.."'?!?") -- can't be reached unless regex is screwed up
|
yellowfive@57
|
149 end
|
yellowfive@57
|
150
|
yellowfive@57
|
151 local function DeserializeNumberHelper(number)
|
yellowfive@57
|
152 --[[ not in 4.3 if number == serNaN then
|
yellowfive@57
|
153 return 0/0
|
yellowfive@106
|
154 else]]if number == serNegInf or number == serNegInfMac then
|
yellowfive@57
|
155 return -inf
|
yellowfive@106
|
156 elseif number == serInf or number == serInfMac then
|
yellowfive@57
|
157 return inf
|
yellowfive@57
|
158 else
|
yellowfive@57
|
159 return tonumber(number)
|
yellowfive@57
|
160 end
|
yellowfive@57
|
161 end
|
yellowfive@57
|
162
|
yellowfive@57
|
163 -- DeserializeValue: worker function for :Deserialize()
|
yellowfive@57
|
164 -- It works in two modes:
|
yellowfive@57
|
165 -- Main (top-level) mode: Deserialize a list of values and return them all
|
yellowfive@57
|
166 -- Recursive (table) mode: Deserialize only a single value (_may_ of course be another table with lots of subvalues in it)
|
yellowfive@57
|
167 --
|
yellowfive@57
|
168 -- The function _always_ works recursively due to having to build a list of values to return
|
yellowfive@57
|
169 --
|
yellowfive@57
|
170 -- Callers are expected to pcall(DeserializeValue) to trap errors
|
yellowfive@57
|
171
|
yellowfive@57
|
172 local function DeserializeValue(iter,single,ctl,data)
|
yellowfive@57
|
173
|
yellowfive@57
|
174 if not single then
|
yellowfive@57
|
175 ctl,data = iter()
|
yellowfive@57
|
176 end
|
yellowfive@57
|
177
|
yellowfive@57
|
178 if not ctl then
|
yellowfive@57
|
179 error("Supplied data misses AceSerializer terminator ('^^')")
|
yellowfive@57
|
180 end
|
yellowfive@57
|
181
|
yellowfive@57
|
182 if ctl=="^^" then
|
yellowfive@57
|
183 -- ignore extraneous data
|
yellowfive@57
|
184 return
|
yellowfive@57
|
185 end
|
yellowfive@57
|
186
|
yellowfive@57
|
187 local res
|
yellowfive@57
|
188
|
yellowfive@57
|
189 if ctl=="^S" then
|
yellowfive@57
|
190 res = gsub(data, "~.", DeserializeStringHelper)
|
yellowfive@57
|
191 elseif ctl=="^N" then
|
yellowfive@57
|
192 res = DeserializeNumberHelper(data)
|
yellowfive@57
|
193 if not res then
|
yellowfive@57
|
194 error("Invalid serialized number: '"..tostring(data).."'")
|
yellowfive@57
|
195 end
|
yellowfive@57
|
196 elseif ctl=="^F" then -- ^F<mantissa>^f<exponent>
|
yellowfive@57
|
197 local ctl2,e = iter()
|
yellowfive@57
|
198 if ctl2~="^f" then
|
yellowfive@57
|
199 error("Invalid serialized floating-point number, expected '^f', not '"..tostring(ctl2).."'")
|
yellowfive@57
|
200 end
|
yellowfive@57
|
201 local m=tonumber(data)
|
yellowfive@57
|
202 e=tonumber(e)
|
yellowfive@57
|
203 if not (m and e) then
|
yellowfive@57
|
204 error("Invalid serialized floating-point number, expected mantissa and exponent, got '"..tostring(m).."' and '"..tostring(e).."'")
|
yellowfive@57
|
205 end
|
yellowfive@57
|
206 res = m*(2^e)
|
yellowfive@57
|
207 elseif ctl=="^B" then -- yeah yeah ignore data portion
|
yellowfive@57
|
208 res = true
|
yellowfive@57
|
209 elseif ctl=="^b" then -- yeah yeah ignore data portion
|
yellowfive@57
|
210 res = false
|
yellowfive@57
|
211 elseif ctl=="^Z" then -- yeah yeah ignore data portion
|
yellowfive@57
|
212 res = nil
|
yellowfive@57
|
213 elseif ctl=="^T" then
|
yellowfive@57
|
214 -- ignore ^T's data, future extensibility?
|
yellowfive@57
|
215 res = {}
|
yellowfive@57
|
216 local k,v
|
yellowfive@57
|
217 while true do
|
yellowfive@57
|
218 ctl,data = iter()
|
yellowfive@57
|
219 if ctl=="^t" then break end -- ignore ^t's data
|
yellowfive@57
|
220 k = DeserializeValue(iter,true,ctl,data)
|
yellowfive@57
|
221 if k==nil then
|
yellowfive@57
|
222 error("Invalid AceSerializer table format (no table end marker)")
|
yellowfive@57
|
223 end
|
yellowfive@57
|
224 ctl,data = iter()
|
yellowfive@57
|
225 v = DeserializeValue(iter,true,ctl,data)
|
yellowfive@57
|
226 if v==nil then
|
yellowfive@57
|
227 error("Invalid AceSerializer table format (no table end marker)")
|
yellowfive@57
|
228 end
|
yellowfive@57
|
229 res[k]=v
|
yellowfive@57
|
230 end
|
yellowfive@57
|
231 else
|
yellowfive@57
|
232 error("Invalid AceSerializer control code '"..ctl.."'")
|
yellowfive@57
|
233 end
|
yellowfive@57
|
234
|
yellowfive@57
|
235 if not single then
|
yellowfive@57
|
236 return res,DeserializeValue(iter)
|
yellowfive@57
|
237 else
|
yellowfive@57
|
238 return res
|
yellowfive@57
|
239 end
|
yellowfive@57
|
240 end
|
yellowfive@57
|
241
|
yellowfive@57
|
242 --- Deserializes the data into its original values.
|
yellowfive@57
|
243 -- Accepts serialized data, ignoring all control characters and whitespace.
|
yellowfive@57
|
244 -- @param str The serialized data (from :Serialize)
|
yellowfive@57
|
245 -- @return true followed by a list of values, OR false followed by an error message
|
yellowfive@57
|
246 function AceSerializer:Deserialize(str)
|
yellowfive@57
|
247 str = gsub(str, "[%c ]", "") -- ignore all control characters; nice for embedding in email and stuff
|
yellowfive@57
|
248
|
yellowfive@57
|
249 local iter = gmatch(str, "(^.)([^^]*)") -- Any ^x followed by string of non-^
|
yellowfive@57
|
250 local ctl,data = iter()
|
yellowfive@57
|
251 if not ctl or ctl~="^1" then
|
yellowfive@57
|
252 -- we purposefully ignore the data portion of the start code, it can be used as an extension mechanism
|
yellowfive@57
|
253 return false, "Supplied data is not AceSerializer data (rev 1)"
|
yellowfive@57
|
254 end
|
yellowfive@57
|
255
|
yellowfive@57
|
256 return pcall(DeserializeValue, iter)
|
yellowfive@57
|
257 end
|
yellowfive@57
|
258
|
yellowfive@57
|
259
|
yellowfive@57
|
260 ----------------------------------------
|
yellowfive@57
|
261 -- Base library stuff
|
yellowfive@57
|
262 ----------------------------------------
|
yellowfive@57
|
263
|
yellowfive@57
|
264 AceSerializer.internals = { -- for test scripts
|
yellowfive@57
|
265 SerializeValue = SerializeValue,
|
yellowfive@57
|
266 SerializeStringHelper = SerializeStringHelper,
|
yellowfive@57
|
267 }
|
yellowfive@57
|
268
|
yellowfive@57
|
269 local mixins = {
|
yellowfive@57
|
270 "Serialize",
|
yellowfive@57
|
271 "Deserialize",
|
yellowfive@57
|
272 }
|
yellowfive@57
|
273
|
yellowfive@57
|
274 AceSerializer.embeds = AceSerializer.embeds or {}
|
yellowfive@57
|
275
|
yellowfive@57
|
276 function AceSerializer:Embed(target)
|
yellowfive@57
|
277 for k, v in pairs(mixins) do
|
yellowfive@57
|
278 target[v] = self[v]
|
yellowfive@57
|
279 end
|
yellowfive@57
|
280 self.embeds[target] = true
|
yellowfive@57
|
281 return target
|
yellowfive@57
|
282 end
|
yellowfive@57
|
283
|
yellowfive@57
|
284 -- Update embeds
|
yellowfive@57
|
285 for target, v in pairs(AceSerializer.embeds) do
|
yellowfive@57
|
286 AceSerializer:Embed(target)
|
yellowfive@106
|
287 end
|