├── LICENSE.md ├── README.md └── smallfolk.lua /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 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 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > Since 2016-02-16, Smallfolk 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 | Smallfolk 5 | ========= 6 | 7 | Smallfolk is a reasonably fast, robust, richtly-featured table serialization 8 | library for Lua. It was specifically written to allow complex data structures 9 | to be loaded from unsafe sources for [LÖVE](http://love2d.org/) games, but can 10 | be used anywhere. 11 | 12 | You use, distribute and extend Smallfolk under the terms of the MIT license. 13 | 14 | Usage 15 | ----- 16 | 17 | Smallfolk is very simple and easy to use: 18 | 19 | ```lua 20 | local smallfolk = require 'smallfolk' 21 | 22 | print(smallfolk.dumps({"Hello", world = true})) 23 | print(smallfolk.loads('{"foo":"bar"}').foo) 24 | -- prints: 25 | -- {"Hello","world":t} 26 | -- bar 27 | ``` 28 | 29 | Fast 30 | ---- 31 | 32 | Using Serpent's benchmark code, Smallfolk's serialization speed is comparable 33 | to that of Ser (and Ser is 33% faster than Serpent). 34 | 35 | It should be noted that deserialization is much slower in Smallfolk than in 36 | most other serialization libraries, because it parses the input itself instead 37 | of handing it over to Lua. However, if you use LuaJIT this difference is much 38 | less, and it is not noticable for small outputs. By default, Smallfolk rejects 39 | inputs that are too large, to prevent DOS attacks. 40 | 41 | Robust 42 | ------ 43 | 44 | Sometimes you have strange, non-euclidean geometries in your table 45 | constructions. It happens, I don't judge. Smallfolk can deal with that, where 46 | some other serialization libraries (or anything that produces JSON) cry "Iä! 47 | Iä! Cthulhu fhtagn!" and give up — or worse, silently produce incorrect 48 | data. 49 | 50 | ```lua 51 | local smallfolk = require 'smallfolk' 52 | 53 | local cthulhu = {{}, {}, {}} 54 | cthulhu.fhtagn = cthulhu 55 | cthulhu[1][cthulhu[2]] = cthulhu[3] 56 | cthulhu[2][cthulhu[1]] = cthulhu[2] 57 | cthulhu[3][cthulhu[3]] = cthulhu 58 | print(smallfolk.dumps(cthulhu)) 59 | -- prints: 60 | -- {{{@2:@3}:{@4:@1}},@3,@4,"fhtagn":@1} 61 | ``` 62 | 63 | Secure 64 | ------ 65 | 66 | Smallfolk doesn't run arbitrary Lua code, so you can safely use it when you 67 | want to read data from an untrusted source. 68 | 69 | Compact 70 | ------- 71 | 72 | Smallfolk creates really small output files compared to something like Ser when 73 | it encounters a lot of non-tree-like data, by using numbered references rather 74 | than item assignment. 75 | 76 | Tested 77 | ------ 78 | 79 | Check out `tests.lua` to see how Smallfolk behaves with all kinds of inputs. 80 | 81 | Reference 82 | --------- 83 | 84 | ###`smallfolk.dumps(object)` 85 | 86 | Returns an 8-bit string representation of `object`. Throws an error if `object` 87 | contains any types that cannot be serialised (userdata, functions and threads). 88 | 89 | ###`smallfolk.loads(string[, maxsize=10000])` 90 | 91 | Returns an object whose representation would be `string`. If the length of 92 | `string` is larger than `maxsize`, no deserialization is attempted and instead 93 | an error is thrown. If `string` is not a valid representation of any object, 94 | an error is thrown. 95 | 96 | See also 97 | -------- 98 | 99 | * [Ser](https://github.com/gvx/Ser): for trusted-source serialization 100 | * [Lady](https://github.com/gvx/Lady): for trusted-source savegames 101 | -------------------------------------------------------------------------------- /smallfolk.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local expect_object, dump_object 3 | local error, tostring, pairs, type, floor, huge, concat = error, tostring, pairs, type, math.floor, math.huge, table.concat 4 | 5 | local dump_type = {} 6 | 7 | function dump_type:string(nmemo, memo, acc) 8 | local nacc = #acc 9 | acc[nacc + 1] = '"' 10 | acc[nacc + 2] = self:gsub('"', '""') 11 | acc[nacc + 3] = '"' 12 | return nmemo 13 | end 14 | 15 | function dump_type:number(nmemo, memo, acc) 16 | acc[#acc + 1] = ("%.17g"):format(self) 17 | return nmemo 18 | end 19 | 20 | function dump_type:table(nmemo, memo, acc) 21 | if memo[self] then 22 | acc[#acc + 1] = '@' 23 | acc[#acc + 1] = tostring(memo[self]) 24 | return nmemo 25 | end 26 | nmemo = nmemo + 1 27 | memo[self] = nmemo 28 | acc[#acc + 1] = '{' 29 | local nself = #self 30 | for i = 1, nself do -- don't use ipairs here, we need the gaps 31 | nmemo = dump_object(self[i], nmemo, memo, acc) 32 | acc[#acc + 1] = ',' 33 | end 34 | for k, v in pairs(self) do 35 | if type(k) ~= 'number' or floor(k) ~= k or k < 1 or k > nself then 36 | nmemo = dump_object(k, nmemo, memo, acc) 37 | acc[#acc + 1] = ':' 38 | nmemo = dump_object(v, nmemo, memo, acc) 39 | acc[#acc + 1] = ',' 40 | end 41 | end 42 | acc[#acc] = acc[#acc] == '{' and '{}' or '}' 43 | return nmemo 44 | end 45 | 46 | function dump_object(object, nmemo, memo, acc) 47 | if object == true then 48 | acc[#acc + 1] = 't' 49 | elseif object == false then 50 | acc[#acc + 1] = 'f' 51 | elseif object == nil then 52 | acc[#acc + 1] = 'n' 53 | elseif object ~= object then 54 | if (''..object):sub(1,1) == '-' then 55 | acc[#acc + 1] = 'N' 56 | else 57 | acc[#acc + 1] = 'Q' 58 | end 59 | elseif object == huge then 60 | acc[#acc + 1] = 'I' 61 | elseif object == -huge then 62 | acc[#acc + 1] = 'i' 63 | else 64 | local t = type(object) 65 | if not dump_type[t] then 66 | error('cannot dump type ' .. t) 67 | end 68 | return dump_type[t](object, nmemo, memo, acc) 69 | end 70 | return nmemo 71 | end 72 | 73 | function M.dumps(object) 74 | local nmemo = 0 75 | local memo = {} 76 | local acc = {} 77 | dump_object(object, nmemo, memo, acc) 78 | return concat(acc) 79 | end 80 | 81 | local function invalid(i) 82 | error('invalid input at position ' .. i) 83 | end 84 | 85 | local nonzero_digit = {['1'] = true, ['2'] = true, ['3'] = true, ['4'] = true, ['5'] = true, ['6'] = true, ['7'] = true, ['8'] = true, ['9'] = true} 86 | local is_digit = {['0'] = true, ['1'] = true, ['2'] = true, ['3'] = true, ['4'] = true, ['5'] = true, ['6'] = true, ['7'] = true, ['8'] = true, ['9'] = true} 87 | local function expect_number(string, start) 88 | local i = start 89 | local head = string:sub(i, i) 90 | if head == '-' then 91 | i = i + 1 92 | head = string:sub(i, i) 93 | end 94 | if nonzero_digit[head] then 95 | repeat 96 | i = i + 1 97 | head = string:sub(i, i) 98 | until not is_digit[head] 99 | elseif head == '0' then 100 | i = i + 1 101 | head = string:sub(i, i) 102 | else 103 | invalid(i) 104 | end 105 | if head == '.' then 106 | local oldi = i 107 | repeat 108 | i = i + 1 109 | head = string:sub(i, i) 110 | until not is_digit[head] 111 | if i == oldi + 1 then 112 | invalid(i) 113 | end 114 | end 115 | if head == 'e' or head == 'E' then 116 | i = i + 1 117 | head = string:sub(i, i) 118 | if head == '+' or head == '-' then 119 | i = i + 1 120 | head = string:sub(i, i) 121 | end 122 | if not is_digit[head] then 123 | invalid(i) 124 | end 125 | repeat 126 | i = i + 1 127 | head = string:sub(i, i) 128 | until not is_digit[head] 129 | end 130 | return tonumber(string:sub(start, i - 1)), i 131 | end 132 | 133 | local expect_object_head = { 134 | t = function(string, i) return true, i end, 135 | f = function(string, i) return false, i end, 136 | n = function(string, i) return nil, i end, 137 | Q = function(string, i) return -(0/0), i end, 138 | N = function(string, i) return 0/0, i end, 139 | I = function(string, i) return 1/0, i end, 140 | i = function(string, i) return -1/0, i end, 141 | ['"'] = function(string, i) 142 | local nexti = i - 1 143 | repeat 144 | nexti = string:find('"', nexti + 1, true) + 1 145 | until string:sub(nexti, nexti) ~= '"' 146 | return string:sub(i, nexti - 2):gsub('""', '"'), nexti 147 | end, 148 | ['0'] = function(string, i) 149 | return expect_number(string, i - 1) 150 | end, 151 | ['{'] = function(string, i, tables) 152 | local nt, k, v = {} 153 | local j = 1 154 | tables[#tables + 1] = nt 155 | if string:sub(i, i) == '}' then 156 | return nt, i + 1 157 | end 158 | while true do 159 | k, i = expect_object(string, i, tables) 160 | if string:sub(i, i) == ':' then 161 | v, i = expect_object(string, i + 1, tables) 162 | nt[k] = v 163 | else 164 | nt[j] = k 165 | j = j + 1 166 | end 167 | local head = string:sub(i, i) 168 | if head == ',' then 169 | i = i + 1 170 | elseif head == '}' then 171 | return nt, i + 1 172 | else 173 | invalid(i) 174 | end 175 | end 176 | end, 177 | ['@'] = function(string, i, tables) 178 | local match = string:match('^%d+', i) 179 | local ref = tonumber(match) 180 | if tables[ref] then 181 | return tables[ref], i + #match 182 | end 183 | invalid(i) 184 | end, 185 | } 186 | expect_object_head['1'] = expect_object_head['0'] 187 | expect_object_head['2'] = expect_object_head['0'] 188 | expect_object_head['3'] = expect_object_head['0'] 189 | expect_object_head['4'] = expect_object_head['0'] 190 | expect_object_head['5'] = expect_object_head['0'] 191 | expect_object_head['6'] = expect_object_head['0'] 192 | expect_object_head['7'] = expect_object_head['0'] 193 | expect_object_head['8'] = expect_object_head['0'] 194 | expect_object_head['9'] = expect_object_head['0'] 195 | expect_object_head['-'] = expect_object_head['0'] 196 | expect_object_head['.'] = expect_object_head['0'] 197 | 198 | expect_object = function(string, i, tables) 199 | local head = string:sub(i, i) 200 | if expect_object_head[head] then 201 | return expect_object_head[head](string, i + 1, tables) 202 | end 203 | invalid(i) 204 | end 205 | 206 | function M.loads(string, maxsize) 207 | if #string > (maxsize or 10000) then 208 | error 'input too large' 209 | end 210 | return (expect_object(string, 1, {})) 211 | end 212 | 213 | return M 214 | --------------------------------------------------------------------------------