mirror of
https://github.com/gvx/Ser.git
synced 2024-11-26 01:44:22 +00:00
Initial commit
This commit is contained in:
commit
736ec710fc
19
LICENSE.md
Normal file
19
LICENSE.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2011,2013 Robin Wellner
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
70
README.md
Normal file
70
README.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
Ser
|
||||||
|
===
|
||||||
|
|
||||||
|
Ser is a fast, robust, richly-featured table serialization library for Lua. It
|
||||||
|
was specifically written to store configuration and save files for
|
||||||
|
[LÖVE](http://love2d.org/) games, but can be used anywhere.
|
||||||
|
|
||||||
|
Originally, this was the code to write save games for
|
||||||
|
[Space](https://github.com/gvx/space), but was released as a stand-alone
|
||||||
|
library after many much-needed improvements.
|
||||||
|
|
||||||
|
Like Space itself, you use, distribute and extend Ser under the terms of the
|
||||||
|
MIT license.
|
||||||
|
|
||||||
|
Simple
|
||||||
|
------
|
||||||
|
|
||||||
|
Ser is very simple and easy to use:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local serialize = require 'ser'
|
||||||
|
|
||||||
|
print(serialize({"Hello", world = true}))
|
||||||
|
-- prints:
|
||||||
|
-- return {"Hello", world = true}
|
||||||
|
```
|
||||||
|
|
||||||
|
Fast
|
||||||
|
----
|
||||||
|
|
||||||
|
Using Serpent's benchmark code, Ser is 33% faster than Serpent.
|
||||||
|
|
||||||
|
Robust
|
||||||
|
------
|
||||||
|
|
||||||
|
Sometimes you have strange, non-euclidean geometries in your table
|
||||||
|
constructions. It happens, I don't judge. Ser can deal with that, where some
|
||||||
|
other serialization libraries cry "Iä! Iä! Cthulhu fhtagn!" and give up —
|
||||||
|
or worse, silently produce incorrect data.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local serialize = require 'ser'
|
||||||
|
|
||||||
|
local cthulhu = {{}, {}, {}}
|
||||||
|
cthulhu.fhtagn = cthulhu
|
||||||
|
cthulhu[1][cthulhu[2]] = cthulhu[3]
|
||||||
|
cthulhu[2][cthulhu[1]] = cthulhu[2]
|
||||||
|
cthulhu[3][cthulhu[3]] = cthulhu
|
||||||
|
print(serialize(cthulhu))
|
||||||
|
-- prints:
|
||||||
|
-- local _3 = {}
|
||||||
|
-- local _2 = {}
|
||||||
|
-- local _1 = {[_2] = _3}
|
||||||
|
-- local _0 = {_1, _2, _3}
|
||||||
|
-- _0.fhtagn = _0
|
||||||
|
-- _2[_1] = _2
|
||||||
|
-- _3[_3] = _0
|
||||||
|
-- return _0
|
||||||
|
```
|
||||||
|
|
||||||
|
Tested
|
||||||
|
------
|
||||||
|
|
||||||
|
Check out `tests.lua` to see how Ser behaves with all kinds of inputs.
|
||||||
|
|
||||||
|
Other solutions
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Check out the [Lua-users wiki](http://lua-users.org/wiki/TableSerialization)
|
||||||
|
for other libraries that do roughly the same thing.
|
121
ser.lua
Normal file
121
ser.lua
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
local pairs, ipairs, tostring, type, concat, dump, floor = pairs, ipairs, tostring, type, table.concat, string.dump, math.floor
|
||||||
|
|
||||||
|
local function getchr(c)
|
||||||
|
return "\\" .. c:byte()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function make_safe(text)
|
||||||
|
return ("%q"):format(text):gsub('\n', 'n'):gsub("[\128-\255]", getchr)
|
||||||
|
end
|
||||||
|
|
||||||
|
local oddvals = {inf = '1/0', ['-inf'] = '-1/0', [tostring(0/0)] = '0/0'}
|
||||||
|
local function write(t, memo, rev_memo)
|
||||||
|
local ty = type(t)
|
||||||
|
if ty == 'number' or ty == 'boolean' or ty == 'nil' then
|
||||||
|
t = tostring(t)
|
||||||
|
return oddvals[t] or t
|
||||||
|
elseif ty == 'string' then
|
||||||
|
return make_safe(t)
|
||||||
|
elseif ty == 'table' or ty == 'function' then
|
||||||
|
if not memo[t] then
|
||||||
|
local index = #rev_memo + 1
|
||||||
|
memo[t] = index
|
||||||
|
rev_memo[index] = t
|
||||||
|
end
|
||||||
|
return '_' .. memo[t]
|
||||||
|
else
|
||||||
|
error("Trying to serialize unsupported type " .. ty)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local kw = {['and'] = true, ['break'] = true, ['do'] = true, ['else'] = true,
|
||||||
|
['elseif'] = true, ['end'] = true, ['false'] = true, ['for'] = true,
|
||||||
|
['function'] = true, ['goto'] = true, ['if'] = true, ['in'] = true,
|
||||||
|
['local'] = true, ['nil'] = true, ['not'] = true, ['or'] = true,
|
||||||
|
['repeat'] = true, ['return'] = true, ['then'] = true, ['true'] = true,
|
||||||
|
['until'] = true, ['while'] = true}
|
||||||
|
local function write_key_value_pair(k, v, memo, rev_memo, name)
|
||||||
|
if type(k) == 'string' and k:match '^[_%a][_%w]*$' and not kw[k] then
|
||||||
|
return (name and name .. '.' or '') .. k ..' = ' .. write(v, memo, rev_memo)
|
||||||
|
else
|
||||||
|
return (name or '') .. '[' .. write(k, memo, rev_memo) .. '] = ' .. write(v, memo, rev_memo)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- fun fact: this function is not perfect
|
||||||
|
-- it has a few false positives sometimes
|
||||||
|
-- but no false negatives, so that's good
|
||||||
|
local function is_cyclic(memo, sub, super)
|
||||||
|
local m = memo[sub]
|
||||||
|
local p = memo[super]
|
||||||
|
return m and p and m < p
|
||||||
|
end
|
||||||
|
|
||||||
|
local function write_table_ex(t, memo, rev_memo, srefs, name)
|
||||||
|
if type(t) == 'function' then
|
||||||
|
return 'local _' .. name .. ' = loadstring ' .. make_safe(dump(t))
|
||||||
|
end
|
||||||
|
local m = {'local _', name, ' = {'}
|
||||||
|
local mi = 3
|
||||||
|
for i = 1, #t do -- don't use ipairs here, we need the gaps
|
||||||
|
local v = t[i]
|
||||||
|
if v == t or is_cyclic(memo, v, t) then
|
||||||
|
srefs[#srefs + 1] = {name, i, v}
|
||||||
|
m[mi + 1] = 'nil, '
|
||||||
|
mi = mi + 1
|
||||||
|
else
|
||||||
|
m[mi + 1] = write(v, memo, rev_memo)
|
||||||
|
m[mi + 2] = ', '
|
||||||
|
mi = mi + 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for k,v in pairs(t) do
|
||||||
|
if type(k) ~= 'number' or floor(k) ~= k or k < 1 or k > #t then
|
||||||
|
if v == t or k == t or is_cyclic(memo, v, t) or is_cyclic(memo, k, t) then
|
||||||
|
srefs[#srefs + 1] = {name, k, v}
|
||||||
|
else
|
||||||
|
m[mi + 1] = write_key_value_pair(k, v, memo, rev_memo)
|
||||||
|
m[mi + 2] = ', '
|
||||||
|
mi = mi + 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
m[mi > 3 and mi or mi + 1] = '}'
|
||||||
|
return concat(m)
|
||||||
|
end
|
||||||
|
|
||||||
|
return function(t)
|
||||||
|
local memo = {[t] = 0}
|
||||||
|
local rev_memo = {[0] = t}
|
||||||
|
local srefs = {}
|
||||||
|
local result = {}
|
||||||
|
|
||||||
|
-- phase 1: recursively descend the table structure
|
||||||
|
local n = 0
|
||||||
|
while rev_memo[n] do
|
||||||
|
result[n + 1] = write_table_ex(rev_memo[n], memo, rev_memo, srefs, n)
|
||||||
|
n = n + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- phase 2: reverse order
|
||||||
|
for i = 1, n*.5 do
|
||||||
|
local j = n - i + 1
|
||||||
|
result[i], result[j] = result[j], result[i]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- phase 3: add all the tricky cyclic stuff
|
||||||
|
for i, v in ipairs(srefs) do
|
||||||
|
n = n + 1
|
||||||
|
result[n] = write_key_value_pair(v[2], v[3], memo, rev_memo, '_' .. v[1])
|
||||||
|
end
|
||||||
|
|
||||||
|
-- phase 4: add something about returning the main table
|
||||||
|
if result[n]:sub(1, 8) == 'local _0' then
|
||||||
|
result[n] = 'return' .. result[n]:sub(11)
|
||||||
|
else
|
||||||
|
result[n + 1] = 'return _0'
|
||||||
|
end
|
||||||
|
|
||||||
|
-- phase 5: just concatenate everything
|
||||||
|
return concat(result, '\n')
|
||||||
|
end
|
69
tests.lua
Normal file
69
tests.lua
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
local serialize = require 'ser'
|
||||||
|
|
||||||
|
local succeeded = 0
|
||||||
|
local failed = 0
|
||||||
|
|
||||||
|
function case(input, expected, message)
|
||||||
|
local output = serialize(input)
|
||||||
|
if output == expected then
|
||||||
|
succeeded = succeeded + 1
|
||||||
|
else
|
||||||
|
print('test failed: ' .. message)
|
||||||
|
print('expected:')
|
||||||
|
print(expected)
|
||||||
|
print('got:')
|
||||||
|
print(output)
|
||||||
|
failed = failed + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function case_error(input, expected, message)
|
||||||
|
local success, err = pcall(serialize, input)
|
||||||
|
if not success and err == expected then
|
||||||
|
succeeded = succeeded + 1
|
||||||
|
else
|
||||||
|
print('test failed: ' .. message)
|
||||||
|
print('expected error:')
|
||||||
|
print(expected)
|
||||||
|
print('got:')
|
||||||
|
print(success, err)
|
||||||
|
failed = failed + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
case({}, 'return {}', 'empty table')
|
||||||
|
|
||||||
|
case({true}, 'return {true}', 'simple table')
|
||||||
|
|
||||||
|
case({{}}, [[
|
||||||
|
local _1 = {}
|
||||||
|
return {_1}]], 'empty table within a table')
|
||||||
|
|
||||||
|
local _t = {}
|
||||||
|
_t.self = _t
|
||||||
|
case(_t, [[local _0 = {}
|
||||||
|
_0.self = _0
|
||||||
|
return _0]], 'simple cycle')
|
||||||
|
|
||||||
|
case_error({coroutine.create(function()end)}, './ser.lua:27: Trying to serialize unsupported type thread', 'unsupported type')
|
||||||
|
|
||||||
|
case({"a", foo = "bar", ["3f"] = true, _1 = false, ["00"] = 9}, 'return {"a", ["3f"] = true, _1 = false, ["00"] = 9, foo = "bar"}', 'various')
|
||||||
|
|
||||||
|
case({'\127\230\255\254\128\12\0128\n\31'}, 'return {"\\127\\230\\255\\254\\128\\12\\0128\\n\\31"}', 'non-ASCII or control characters in string value')
|
||||||
|
|
||||||
|
case({['\127\230\255\254\128\12\0128\n\31'] = '\0'}, 'return {["\\127\\230\\255\\254\\128\\12\\0128\\n\\31"] = "\\0"}', 'non-ASCII or control characters in string key')
|
||||||
|
|
||||||
|
local x = {}
|
||||||
|
case({x, {x}, x}, [[
|
||||||
|
local _2 = {}
|
||||||
|
local _1 = {}
|
||||||
|
local _0 = {_1, _2, _1}
|
||||||
|
_2[1] = _1
|
||||||
|
return _0]], 'repeated table')
|
||||||
|
|
||||||
|
case({['end'] = true, ['false'] = false}, 'return {["false"] = false, ["end"] = true}', 'keywords as table keys')
|
||||||
|
|
||||||
|
case({1/0, -1/0, 0/0}, 'return {1/0, -1/0, 0/0}', 'representation of infinity and NaN')
|
||||||
|
|
||||||
|
print(failed .. ' tests failed')
|
||||||
|
print(succeeded .. ' tests succeeded')
|
Loading…
Reference in New Issue
Block a user