278 lines
6.7 KiB
Lua
278 lines
6.7 KiB
Lua
--- **AceSerializer-3.0** Serializes and deserializes Lua tables into strings.
|
|
-- Can serialize any Lua table (including nested tables) into a string,
|
|
-- and deserialize that string back into a table.
|
|
-- @class file
|
|
-- @name AceSerializer-3.0
|
|
-- @release $Id: AceSerializer-3.0.lua 1284 2022-09-25 09:15:30Z nevcairiel $
|
|
local MAJOR, MINOR = "AceSerializer-3.0", 3
|
|
|
|
local AceSerializer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
|
|
|
|
if not AceSerializer then return end
|
|
|
|
-- Lua APIs
|
|
local assert, error, pcall = assert, error, pcall
|
|
local type, tostring, tonumber = type, tostring, tonumber
|
|
local strfind, strsub, strjoin, strlen = string.find, string.sub, string.join, string.len
|
|
local max, min = math.max, math.min
|
|
|
|
-- quick-and-dirty nil value serializer, used to sanitize the tables before serializing
|
|
local function SerializeValue(v, res, n)
|
|
-- nil
|
|
if v == nil then
|
|
res[n+1] = "n"
|
|
return n+1
|
|
end
|
|
|
|
-- boolean
|
|
if type(v) == "boolean" then
|
|
res[n+1] = v and "t" or "f"
|
|
return n+1
|
|
end
|
|
|
|
-- number
|
|
if type(v) == "number" then
|
|
-- force 4-decimal numbers, others as-is
|
|
local str = tostring(v)
|
|
if strfind(str, "[^0-9%.]") then
|
|
res[n+1] = str
|
|
else
|
|
res[n+1] = format("%.4f", v)
|
|
end
|
|
return n+1
|
|
end
|
|
|
|
-- string
|
|
if type(v) == "string" then
|
|
res[n+1] = format("%q", v)
|
|
return n+1
|
|
end
|
|
|
|
-- table
|
|
if type(v) == "table" then
|
|
res[n+1] = "{"
|
|
local n2 = n+2
|
|
for k, val in pairs(v) do
|
|
n2 = SerializeValue(k, res, n2)
|
|
res[n2+1] = "="
|
|
n2 = n2 + 2
|
|
n2 = SerializeValue(val, res, n2)
|
|
res[n2+1] = ","
|
|
n2 = n2 + 1
|
|
end
|
|
res[n2] = "}"
|
|
return n2
|
|
end
|
|
|
|
-- anything else (we don't know how to serialize)
|
|
error(format("Cannot serialize a value of type %s", type(v)))
|
|
end
|
|
|
|
local function Serialize(t)
|
|
if type(t) ~= "table" then
|
|
error("Usage: AceSerializer:Serialize(tbl): tbl must be a table, got " .. type(t), 2)
|
|
end
|
|
local s = {}
|
|
SerializeValue(t, s, 0)
|
|
return strjoin("", s)
|
|
end
|
|
|
|
AceSerializer.Serialize = function(self, t)
|
|
local target = (self == AceSerializer or (type(self) == "table" and AceSerializer.embeds[self])) and t or self
|
|
return Serialize(target)
|
|
end
|
|
|
|
local function Deserialize(s)
|
|
if type(s) ~= "string" then
|
|
error("Usage: AceSerializer:Deserialize(str): str must be a string, got " .. type(s), 2)
|
|
end
|
|
|
|
local stack = {}
|
|
local n = strlen(s)
|
|
local pos = 1
|
|
|
|
-- read a value
|
|
local function ReadValue()
|
|
-- skip whitespace
|
|
while pos <= n and strfind(strsub(s, pos, pos), "%s") do
|
|
pos = pos + 1
|
|
end
|
|
|
|
if pos > n then error("Empty string") end
|
|
|
|
local c = strsub(s, pos, pos)
|
|
pos = pos + 1
|
|
|
|
if c == "n" then
|
|
return nil
|
|
elseif c == "t" then
|
|
return true
|
|
elseif c == "f" then
|
|
return false
|
|
elseif c == "{" then
|
|
local tbl = {}
|
|
local numkey = 0
|
|
local key
|
|
while pos <= n do
|
|
-- skip whitespace
|
|
while pos <= n and strfind(strsub(s, pos, pos), "%s") do
|
|
pos = pos + 1
|
|
end
|
|
if pos > n then error("Missing closing brace") end
|
|
if strsub(s, pos, pos) == "}" then
|
|
pos = pos + 1
|
|
break
|
|
end
|
|
-- read key (non-number key)
|
|
if strsub(s, pos, pos) ~= "[" then
|
|
-- assume it's a string key
|
|
key = ReadValue()
|
|
else
|
|
pos = pos + 1
|
|
key = ReadValue()
|
|
if strsub(s, pos, pos) ~= "]" then error("Missing ]") end
|
|
pos = pos + 1
|
|
end
|
|
-- skip whitespace and =
|
|
while pos <= n and strfind(strsub(s, pos, pos), "[=%s]") do
|
|
pos = pos + 1
|
|
end
|
|
local val = ReadValue()
|
|
if key then
|
|
tbl[key] = val
|
|
else
|
|
numkey = numkey + 1
|
|
tbl[numkey] = val
|
|
end
|
|
-- skip whitespace and comma
|
|
while pos <= n and strfind(strsub(s, pos, pos), "[,%s]") do
|
|
pos = pos + 1
|
|
end
|
|
end
|
|
return tbl
|
|
elseif c == "\"" then
|
|
local i = pos
|
|
repeat
|
|
if i > n then error("Unterminated string") end
|
|
until strfind(strsub(s, i, i), "[^\"]") or i == n
|
|
local str = strsub(s, pos, i-1)
|
|
pos = i + 1
|
|
return str
|
|
else
|
|
-- number or error
|
|
local numstr = ""
|
|
while pos <= n and strfind(strsub(s, pos, pos), "[0-9%.%-]") do
|
|
numstr = numstr .. strsub(s, pos, pos)
|
|
pos = pos + 1
|
|
end
|
|
if numstr == "" then
|
|
error("Invalid number at position " .. pos)
|
|
end
|
|
return tonumber(numstr)
|
|
end
|
|
end
|
|
|
|
local value = ReadValue()
|
|
-- skip whitespace
|
|
while pos <= n and strfind(strsub(s, pos, pos), "%s") do
|
|
pos = pos + 1
|
|
end
|
|
if pos <= n then
|
|
error("Trailing characters after serialized table: " .. strsub(s, pos))
|
|
end
|
|
return value
|
|
end
|
|
|
|
AceSerializer.Deserialize = function(self, s)
|
|
local str = (self == AceSerializer or (type(self) == "table" and AceSerializer.embeds[self])) and s or self
|
|
local ok, res = pcall(Deserialize, str)
|
|
if ok then return true, res else return false, res end
|
|
end
|
|
|
|
-- http://lua-users.org/wiki/Base64EncoderAndDecoder
|
|
local b64 = {
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
"abcdefghijklmnopqrstuvwxyz",
|
|
"0123456789+/",
|
|
}
|
|
|
|
local function EncodeString(str)
|
|
local encoded = ""
|
|
for i = 1, strlen(str), 3 do
|
|
local b1, b2, b3 = strbyte(str, i, i+2)
|
|
if not b3 then
|
|
b3 = 0
|
|
end
|
|
if not b2 then
|
|
b2 = 0
|
|
end
|
|
local n = b1 * 256 + b2 * 256 + b3
|
|
local e1, e2, e3, e4 = (n/4)%64, (n/4)%64, (n/4)%64, n%64
|
|
encoded = encoded .. strsub(b64[1], e1, e1) .. strsub(b64[1], e2, e2) .. strsub(b64[3], e3, e3) .. strsub(b64[3], e4, e4)
|
|
end
|
|
--[[
|
|
-- fix padding at the end to be lua-friendly
|
|
if mod(len(str),3)==1 then
|
|
encoded = strsub(encoded, 1, -4) .. "=="
|
|
elseif mod(len(str),3)==2 then
|
|
encoded = strsub(encoded, 1, -2) .. "="
|
|
end
|
|
]]
|
|
return encoded
|
|
end
|
|
|
|
local function DecodeString(str)
|
|
local decoded = ""
|
|
str = gsub(str, "%s", "")
|
|
local len = strlen(str)
|
|
local i = 1
|
|
while i <= len do
|
|
local e1, e2, e3, e4 = strfind(str, "(.)(.)(.?)(.?)", i)
|
|
if not e1 then error("Invalid string") end
|
|
local n = (strfind(b64[1], e1) - 1) * 64 + (strfind(b64[1], e2) - 1)
|
|
n = n * 64 + (strfind(b64[3], e3) - 1)
|
|
n = n * 64 + (strfind(b64[3], e4) - 1)
|
|
decoded = decoded .. strchar(n/256, n%256)
|
|
if e4 == "=" then
|
|
decoded = strsub(decoded, 1, -2)
|
|
elseif e3 == "=" then
|
|
decoded = strsub(decoded, 1, -3)
|
|
end
|
|
i = e4 + 1
|
|
end
|
|
return decoded
|
|
end
|
|
|
|
function AceSerializer:SerializeForPrint(val)
|
|
return EncodeString(Serialize(val))
|
|
end
|
|
|
|
function AceSerializer:DeserializeFromPrint(str)
|
|
local success, val = pcall(Deserialize, DecodeString(str))
|
|
if success then
|
|
return val
|
|
end
|
|
return nil, "Invalid serialization string"
|
|
end
|
|
|
|
AceSerializer.embeds = AceSerializer.embeds or {}
|
|
|
|
local function Embed(target)
|
|
for method, func in pairs(AceSerializer) do
|
|
if type(func) == "function" and method ~= "Embed" and method ~= "embeds" then
|
|
target[method] = func
|
|
end
|
|
end
|
|
end
|
|
|
|
function AceSerializer:Embed(target)
|
|
Embed(target)
|
|
target.embeds = AceSerializer.embeds
|
|
AceSerializer.embeds[target] = true
|
|
return target
|
|
end
|
|
|
|
for target, v in pairs(AceSerializer.embeds) do
|
|
Embed(target)
|
|
end
|