Files
Freewood-OctoWoW-Core/Core/Export.lua
T

274 lines
8.4 KiB
Lua

-- Freewood-OctoWoW-Core: Export und Import als kurzer Code
--
-- Format: FW1:<art>:<pruefsumme>:<koerper>
-- art kleingeschriebene Datenart, z. B. "pets" oder "beasts"
-- pruefsumme 8 Hex-Zeichen ueber den Koerper (erkennt Tippfehler und abgeschnittene Codes)
-- koerper Base64( Komprimiert( CBOR( { v, k, t, a, d } ) ) )
-- Das ist Komprimierung und Kodierung, keine Verschluesselung.
--
-- Sicherheit beim Import:
-- * Es wird nie Code ausgefuehrt, nur Daten gelesen (CBOR).
-- * Groessenlimit, Tiefenlimit, Knotenlimit, nur Text/Zahl/Wahrheitswert/Tabelle.
-- * Das zustaendige Modul prueft die Daten zusaetzlich (validate) und fuegt sie
-- selbst zusammen (import). Regel fuer Module: importierte Eintraege mit
-- src = "import" markieren und eigene Daten nie ueberschreiben.
--
-- Ein Modul meldet sich so an:
-- Freewood.Export.RegisterHandler("pets", {
-- label = "Jaeger-Begleiter" (Text oder Funktion),
-- export = function() return <Tabelle> end,
-- validate = function(data, payload) return true end, -- optional
-- import = function(data, payload) return "3 neu, 1 ergaenzt" end,
-- })
local ADDON, FW = ...
local Export = {}
FW.Export = Export
local Util = FW.Util
local FORMAT_VERSION = 1
local MAX_ENCODED = 150000
local HARD_MAX = 400000 -- groesstes erlaubtes Limit eines Handlers (handler.maxEncoded)
local LIMITS = {
maxDepth = 12,
maxNodes = 200000,
maxString = 4096,
}
Export.handlers = {}
Export.handlerList = {}
function Export.RegisterHandler(kind, handler)
assert(type(kind) == "string" and kind:match("^[a-z0-9]+$"),
"Freewood.Export: Datenart muss aus a-z und 0-9 bestehen")
assert(type(handler) == "table", "Freewood.Export: Handler fehlt")
handler.kind = kind
if not Export.handlers[kind] then
Export.handlerList[#Export.handlerList + 1] = handler
else
for i, h in ipairs(Export.handlerList) do
if h.kind == kind then
Export.handlerList[i] = handler
end
end
end
Export.handlers[kind] = handler
end
function Export.GetLabel(handler)
local label = handler.label
if type(label) == "function" then
label = label()
end
return label or handler.kind
end
local function GetEncoder()
local E = C_EncodingUtil
if E and E.SerializeCBOR and E.DeserializeCBOR and E.CompressString
and E.DecompressString and E.EncodeBase64 and E.DecodeBase64 then
return E
end
return nil
end
-- Prueft eine entpackte Datenstruktur auf harmlose Form.
function Export.CheckStructure(data)
if type(data) ~= "table" then
return false
end
local nodes = 0
local seen = {}
local stack = { { data, 1 } }
while #stack > 0 do
local entry = table.remove(stack)
local tbl, depth = entry[1], entry[2]
if depth > LIMITS.maxDepth or seen[tbl] then
return false
end
seen[tbl] = true
for k, v in pairs(tbl) do
nodes = nodes + 1
if nodes > LIMITS.maxNodes then
return false
end
local kt = type(k)
if kt ~= "string" and kt ~= "number" then
return false
end
if kt == "string" and #k > LIMITS.maxString then
return false
end
local vt = type(v)
if vt == "string" then
if #v > LIMITS.maxString then
return false
end
elseif vt == "number" then
if v ~= v or v == math.huge or v == -math.huge then
return false
end
elseif vt == "table" then
stack[#stack + 1] = { v, depth + 1 }
elseif vt ~= "boolean" then
return false
end
end
end
return true
end
-- Erzeugt den Code. Rueckgabe: text oder nil, fehlerSchluessel
function Export.Encode(kind, data)
local E = GetEncoder()
if not E then
return nil, "ERR_NOAPI"
end
if type(kind) ~= "string" or not kind:match("^[a-z0-9]+$") or type(data) ~= "table" then
return nil, "ERR_INVALID"
end
local payload = { v = FORMAT_VERSION, k = kind, t = time(), a = FW.version, d = data }
local ok, cbor = pcall(E.SerializeCBOR, payload)
if not ok or type(cbor) ~= "string" then
return nil, "ERR_INVALID"
end
local ok2, packed = pcall(E.CompressString, cbor)
if not ok2 or type(packed) ~= "string" then
return nil, "ERR_INVALID"
end
local ok3, body = pcall(E.EncodeBase64, packed)
if not ok3 or type(body) ~= "string" then
return nil, "ERR_INVALID"
end
local hLimit = Export.handlers[kind] and tonumber(Export.handlers[kind].maxEncoded) or MAX_ENCODED
if #body > math.min(hLimit, HARD_MAX) then
return nil, "ERR_TOO_LARGE"
end
return string.format("FW%d:%s:%s:%s", FORMAT_VERSION, kind, Util.Hash(body), body)
end
-- Liest einen Code. Rueckgabe: payload oder nil, fehlerSchluessel
function Export.Decode(text)
if type(text) ~= "string" then
return nil, "ERR_FORMAT"
end
text = text:gsub("%s+", "")
if text == "" then
return nil, "ERR_FORMAT"
end
if #text > HARD_MAX + 64 then
return nil, "ERR_TOO_LARGE"
end
local version = text:match("^FW(%d+):")
if not version then
return nil, "ERR_FORMAT"
end
if tonumber(version) > FORMAT_VERSION then
return nil, "ERR_VERSION"
end
local kind, crc, body = text:match("^FW%d+:([a-z0-9]+):(%x%x%x%x%x%x%x%x):(.+)$")
if not kind then
return nil, "ERR_FORMAT"
end
if body:find("[^%w%+/=_%-]") then
return nil, "ERR_FORMAT"
end
local hl = Export.handlers[kind] and tonumber(Export.handlers[kind].maxEncoded) or MAX_ENCODED
if #body > math.min(hl, HARD_MAX) then
return nil, "ERR_TOO_LARGE"
end
if Util.Hash(body) ~= crc:lower() then
return nil, "ERR_CHECKSUM"
end
local E = GetEncoder()
if not E then
return nil, "ERR_NOAPI"
end
local ok, packed = pcall(E.DecodeBase64, body)
if not ok or type(packed) ~= "string" then
return nil, "ERR_DECODE"
end
local ok2, cbor = pcall(E.DecompressString, packed)
if not ok2 or type(cbor) ~= "string" then
return nil, "ERR_DECODE"
end
local ok3, payload = pcall(E.DeserializeCBOR, cbor)
if not ok3 or type(payload) ~= "table" then
return nil, "ERR_DECODE"
end
if payload.v ~= FORMAT_VERSION then
if type(payload.v) == "number" and payload.v > FORMAT_VERSION then
return nil, "ERR_VERSION"
end
return nil, "ERR_INVALID"
end
if payload.k ~= kind or type(payload.d) ~= "table" then
return nil, "ERR_INVALID"
end
if not Export.CheckStructure(payload.d) then
return nil, "ERR_INVALID"
end
return payload
end
-- Exportiert die Daten eines Moduls. Rueckgabe: text oder nil, fehlerSchluessel, extra
function Export.ExportKind(kind)
local handler = Export.handlers[kind]
if not handler or not handler.export then
return nil, "ERR_NO_HANDLER", kind
end
local ok, data = pcall(handler.export)
if not ok or type(data) ~= "table" then
return nil, "ERR_INVALID"
end
if next(data) == nil then
return nil, "EXCHANGE_NOTHING_EXPORT"
end
-- encodeAs: ein Handler darf unter der Art eines anderen Handlers exportieren (dann importiert dieser)
return Export.Encode(handler.encodeAs or kind, data)
end
-- Importiert einen Code ueber das zustaendige Modul.
-- Rueckgabe: true, statistik, art oder false, fehlerSchluessel, extra
function Export.Import(text)
local payload, err = Export.Decode(text)
if not payload then
return false, err
end
local handler = Export.handlers[payload.k]
if not handler or not handler.import then
return false, "ERR_NO_HANDLER", payload.k
end
if handler.validate then
local ok, valid, message = pcall(handler.validate, payload.d, payload)
if not ok or not valid then
return false, "ERR_INVALID", type(message) == "string" and message or nil
end
end
local ok, result = pcall(handler.import, payload.d, payload)
if not ok then
FW:Debug("Import fehlgeschlagen:", result)
return false, "ERR_INVALID"
end
return true, result, payload.k
end