├── LICENSE.md ├── README.md ├── bigtest.lua ├── ser.lua └── tests.lua /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011,2013 Robin Wellner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > Since 2016-02-16, Ser is **deprecated**. I will still fix reported bugs, but for new projects, I recommend [bitser](https://github.com/gvx/bitser) if you're using LuaJIT, and [binser](https://github.com/bakpakin/binser) 2 | otherwise. 3 | 4 | Ser 5 | === 6 | 7 | Ser is a fast, robust, richly-featured table serialization library for Lua. It 8 | was specifically written to store configuration and save files for 9 | [LÖVE](http://love2d.org/) games, but can be used anywhere. 10 | 11 | Originally, this was the code to write save games for 12 | [Space](https://github.com/gvx/space), but was released as a stand-alone 13 | library after many much-needed improvements. 14 | 15 | Like Space itself, you use, distribute and extend Ser under the terms of the 16 | MIT license. 17 | 18 | Simple 19 | ------ 20 | 21 | Ser is very simple and easy to use: 22 | 23 | ```lua 24 | local serialize = require 'ser' 25 | 26 | print(serialize({"Hello", world = true})) 27 | -- prints: 28 | -- return {"Hello", world = true} 29 | ``` 30 | 31 | Fast 32 | ---- 33 | 34 | Using Serpent's benchmark code, Ser is 33% faster than Serpent. 35 | 36 | Robust 37 | ------ 38 | 39 | Sometimes you have strange, non-euclidean geometries in your table 40 | constructions. It happens, I don't judge. Ser can deal with that, where some 41 | other serialization libraries cry "Iä! Iä! Cthulhu fhtagn!" and give up — 42 | or worse, silently produce incorrect data. 43 | 44 | ```lua 45 | local serialize = require 'ser' 46 | 47 | local cthulhu = {{}, {}, {}} 48 | cthulhu.fhtagn = cthulhu 49 | cthulhu[1][cthulhu[2]] = cthulhu[3] 50 | cthulhu[2][cthulhu[1]] = cthulhu[2] 51 | cthulhu[3][cthulhu[3]] = cthulhu 52 | print(serialize(cthulhu)) 53 | -- prints: 54 | -- local _3 = {} 55 | -- local _2 = {} 56 | -- local _1 = {[_2] = _3} 57 | -- local _0 = {_1, _2, _3} 58 | -- _0.fhtagn = _0 59 | -- _2[_1] = _2 60 | -- _3[_3] = _0 61 | -- return _0 62 | ``` 63 | 64 | Tested 65 | ------ 66 | 67 | Check out `tests.lua` to see how Ser behaves with all kinds of inputs. 68 | 69 | Other solutions 70 | --------------- 71 | 72 | Check out the [Lua-users wiki](http://lua-users.org/wiki/TableSerialization) 73 | for other libraries that do roughly the same thing. 74 | 75 | See also 76 | -------- 77 | 78 | * [Lady](https://github.com/gvx/Lady): for trusted-source savegames 79 | * [Smallfolk](https://github.com/gvx/Smallfolk): for untrusted-source serialization 80 | -------------------------------------------------------------------------------- /bigtest.lua: -------------------------------------------------------------------------------- 1 | local serialize = require 'ser' 2 | 3 | local t = {} 4 | for i = 1, 15 do 5 | t[i] = {} 6 | for j = 1, 10 do 7 | t[i][j] = {{'woo'}} 8 | end 9 | end 10 | local s = serialize(t) 11 | print(s) 12 | loadstring(s)() 13 | -------------------------------------------------------------------------------- /ser.lua: -------------------------------------------------------------------------------- 1 | local pairs, ipairs, tostring, type, concat, dump, floor, format = pairs, ipairs, tostring, type, table.concat, string.dump, math.floor, string.format 2 | 3 | local function getchr(c) 4 | return "\\" .. c:byte() 5 | end 6 | 7 | local function make_safe(text) 8 | return ("%q"):format(text):gsub('\n', 'n'):gsub("[\128-\255]", getchr) 9 | end 10 | 11 | local oddvals = {[tostring(1/0)] = '1/0', [tostring(-1/0)] = '-1/0', [tostring(-(0/0))] = '-(0/0)', [tostring(0/0)] = '0/0'} 12 | local function write(t, memo, rev_memo) 13 | local ty = type(t) 14 | if ty == 'number' then 15 | t = format("%.17g", t) 16 | return oddvals[t] or t 17 | elseif ty == 'boolean' or ty == 'nil' then 18 | return tostring(t) 19 | elseif ty == 'string' then 20 | return make_safe(t) 21 | elseif ty == 'table' or ty == 'function' then 22 | if not memo[t] then 23 | local index = #rev_memo + 1 24 | memo[t] = index 25 | rev_memo[index] = t 26 | end 27 | return '_[' .. memo[t] .. ']' 28 | else 29 | error("Trying to serialize unsupported type " .. ty) 30 | end 31 | end 32 | 33 | local kw = {['and'] = true, ['break'] = true, ['do'] = true, ['else'] = true, 34 | ['elseif'] = true, ['end'] = true, ['false'] = true, ['for'] = true, 35 | ['function'] = true, ['goto'] = true, ['if'] = true, ['in'] = true, 36 | ['local'] = true, ['nil'] = true, ['not'] = true, ['or'] = true, 37 | ['repeat'] = true, ['return'] = true, ['then'] = true, ['true'] = true, 38 | ['until'] = true, ['while'] = true} 39 | local function write_key_value_pair(k, v, memo, rev_memo, name) 40 | if type(k) == 'string' and k:match '^[_%a][_%w]*$' and not kw[k] then 41 | return (name and name .. '.' or '') .. k ..'=' .. write(v, memo, rev_memo) 42 | else 43 | return (name or '') .. '[' .. write(k, memo, rev_memo) .. ']=' .. write(v, memo, rev_memo) 44 | end 45 | end 46 | 47 | -- fun fact: this function is not perfect 48 | -- it has a few false positives sometimes 49 | -- but no false negatives, so that's good 50 | local function is_cyclic(memo, sub, super) 51 | local m = memo[sub] 52 | local p = memo[super] 53 | return m and p and m < p 54 | end 55 | 56 | local function write_table_ex(t, memo, rev_memo, srefs, name) 57 | if type(t) == 'function' then 58 | return '_[' .. name .. ']=loadstring' .. make_safe(dump(t)) 59 | end 60 | local m = {} 61 | local mi = 1 62 | for i = 1, #t do -- don't use ipairs here, we need the gaps 63 | local v = t[i] 64 | if v == t or is_cyclic(memo, v, t) then 65 | srefs[#srefs + 1] = {name, i, v} 66 | m[mi] = 'nil' 67 | mi = mi + 1 68 | else 69 | m[mi] = write(v, memo, rev_memo) 70 | mi = mi + 1 71 | end 72 | end 73 | for k,v in pairs(t) do 74 | if type(k) ~= 'number' or floor(k) ~= k or k < 1 or k > #t then 75 | if v == t or k == t or is_cyclic(memo, v, t) or is_cyclic(memo, k, t) then 76 | srefs[#srefs + 1] = {name, k, v} 77 | else 78 | m[mi] = write_key_value_pair(k, v, memo, rev_memo) 79 | mi = mi + 1 80 | end 81 | end 82 | end 83 | return '_[' .. name .. ']={' .. concat(m, ',') .. '}' 84 | end 85 | 86 | return function(t) 87 | local memo = {[t] = 0} 88 | local rev_memo = {[0] = t} 89 | local srefs = {} 90 | local result = {} 91 | 92 | -- phase 1: recursively descend the table structure 93 | local n = 0 94 | while rev_memo[n] do 95 | result[n + 1] = write_table_ex(rev_memo[n], memo, rev_memo, srefs, n) 96 | n = n + 1 97 | end 98 | 99 | -- phase 2: reverse order 100 | for i = 1, n*.5 do 101 | local j = n - i + 1 102 | result[i], result[j] = result[j], result[i] 103 | end 104 | 105 | -- phase 3: add all the tricky cyclic stuff 106 | for i, v in ipairs(srefs) do 107 | n = n + 1 108 | result[n] = write_key_value_pair(v[2], v[3], memo, rev_memo, '_[' .. v[1] .. ']') 109 | end 110 | 111 | -- phase 4: add something about returning the main table 112 | if result[n]:sub(1, 5) == '_[0]=' then 113 | result[n] = 'return ' .. result[n]:sub(6) 114 | else 115 | result[n + 1] = 'return _[0]' 116 | end 117 | 118 | -- phase 5: just concatenate everything 119 | result = concat(result, '\n') 120 | return n > 1 and 'local _={}\n' .. result or result 121 | end 122 | -------------------------------------------------------------------------------- /tests.lua: -------------------------------------------------------------------------------- 1 | local serialize = require 'ser' 2 | 3 | local succeeded = 0 4 | local failed = 0 5 | 6 | function case(input, expected, message) 7 | local output = serialize(input) 8 | if output == expected then 9 | succeeded = succeeded + 1 10 | else 11 | print('test failed: ' .. message) 12 | print('expected:') 13 | print(expected) 14 | print('got:') 15 | print(output) 16 | failed = failed + 1 17 | end 18 | end 19 | 20 | function case_error(input, expected, message) 21 | local success, err = pcall(serialize, input) 22 | if not success and err == expected then 23 | succeeded = succeeded + 1 24 | else 25 | print('test failed: ' .. message) 26 | print('expected error:') 27 | print(expected) 28 | print('got:') 29 | print(success, err) 30 | failed = failed + 1 31 | end 32 | end 33 | 34 | case({}, 'return {}', 'empty table') 35 | 36 | case({true}, 'return {true}', 'simple table') 37 | 38 | case({{}}, [[ 39 | local _={} 40 | _[1]={} 41 | return {_[1]}]], 'empty table within a table') 42 | 43 | local _t = {} 44 | _t.self = _t 45 | case(_t, [=[local _={} 46 | _[0]={} 47 | _[0].self=_[0] 48 | return _[0]]=], 'simple cycle') 49 | 50 | case_error({coroutine.create(function()end)}, './ser.lua:29: Trying to serialize unsupported type thread', 'unsupported type') 51 | 52 | case({"a", foo = "bar", ["3f"] = true, _1 = false, ["00"] = 9}, 'return {"a",["3f"]=true,_1=false,["00"]=9,foo="bar"}', 'various') 53 | 54 | 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') 55 | 56 | 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') 57 | 58 | local x = {} 59 | case({x, {x}, x}, [=[ 60 | local _={} 61 | _[2]={nil} 62 | _[1]={} 63 | _[0]={_[1],_[2],_[1]} 64 | _[2][1]=_[1] 65 | return _[0]]=], 'repeated table') 66 | 67 | case({['end'] = true, ['false'] = false}, 'return {["false"]=false,["end"]=true}', 'keywords as table keys') 68 | 69 | case({1/0, -1/0, 0/0}, 'return {1/0,-1/0,0/0}', 'representation of infinity and NaN') 70 | 71 | print(failed .. ' tests failed') 72 | print(succeeded .. ' tests succeeded') 73 | --------------------------------------------------------------------------------