diff --git a/src/lib/bitser/.travis.yml b/src/lib/bitser/.travis.yml new file mode 100644 index 0000000..0b131a4 --- /dev/null +++ b/src/lib/bitser/.travis.yml @@ -0,0 +1,26 @@ +language: python +sudo: false + +env: + - LUA="luajit=2.0" + +before_install: + - pip install hererocks + - hererocks lua_install -r^ --$LUA + - export PATH=$PATH:$PWD/lua_install/bin + +install: + - luarocks install luacheck + - luarocks install busted + - luarocks install luacov + - luarocks install luacov-coveralls + - luarocks install middleclass + - wget https://bitbucket.org/bartbes/slither/raw/bbd85db19b19d9d14470b4fa7b73029b6c070fb0/slither.lua + - wget https://raw.githubusercontent.com/vrld/hump/038bc9025f1cb850355f4b073357b087b8122da9/class.lua + +script: + - luacheck --std max+busted bitser.lua spec --globals love + - busted --verbose --coverage + +after_success: + - luacov-coveralls --include bitser -e $TRAVIS_BUILD_DIR/lua_install diff --git a/src/lib/bitser/README.md b/src/lib/bitser/README.md new file mode 100644 index 0000000..68a5f61 --- /dev/null +++ b/src/lib/bitser/README.md @@ -0,0 +1,53 @@ +# bitser + +[![Build Status](https://travis-ci.org/gvx/bitser.svg?branch=master)](https://travis-ci.org/gvx/bitser) +[![Coverage Status](https://coveralls.io/repos/github/gvx/bitser/badge.svg?branch=master)](https://coveralls.io/github/gvx/bitser?branch=master) + +Serializes and deserializes Lua values with LuaJIT. + +```lua +local bitser = require 'bitser' + +bitser.register('someResource', someResource) +bitser.registerClass(SomeClass) + +serializedString = bitser.dumps(someValue) +someValue = bitser.loads(serializedString) +``` + +Documentation can be found in [USAGE.md](USAGE.md). + +Pull requests, bug reports and other feedback welcome! :heart: + +Bitser is released under the ISC license (functionally equivalent to the BSD +2-Clause and MIT licenses). + +Please note that bitser requires LuaJIT for its `ffi` library and JIT compilation. Without JIT, it may or may not run, but it will be much slower than usual. This primarily affects Android and iOS, because JIT is disabled on those platforms. + +## Why would I use this? + +Because it's fast. Because it produces tiny output. Because the name means "snappier" +or "unfriendlier" in Dutch. Because it's safe to use with untrusted data. + +Because it's inspired by [binser](https://github.com/bakpakin/binser), which is great. + +## How do I use the benchmark thingy? + +Download zero or more of [binser.lua](https://raw.githubusercontent.com/bakpakin/binser/master/binser.lua), +[ser.lua](https://raw.githubusercontent.com/gvx/Ser/master/ser.lua), +[smallfolk.lua](https://raw.githubusercontent.com/gvx/Smallfolk/master/smallfolk.lua), +[serpent.lua](https://raw.githubusercontent.com/pkulchenko/serpent/master/src/serpent.lua) and +[MessagePack.lua](https://raw.githubusercontent.com/fperrad/lua-MessagePack/master/src/MessagePack.lua), and run: + + love . + +You do need [LÖVE](https://love2d.org/) for that. + +You can add more cases in the folder `cases/` (check out `_new.lua`), and add other +serializers to the benchmark in `main.lua`. If you do either of those things, please +send me a pull request! + +## You can register classes? + +Yes. At the moment, bitser supports MiddleClass, SECL, hump.class, Slither and Moonscript classes (and +probably some other class libraries by accident). diff --git a/src/lib/bitser/USAGE.md b/src/lib/bitser/USAGE.md new file mode 100644 index 0000000..f7be3cc --- /dev/null +++ b/src/lib/bitser/USAGE.md @@ -0,0 +1,234 @@ +* [Basic usage](#basic-usage) +* [Serializing class instances](#serializing-class-instances) +* [Advanced usage](#advanced-usage) +* [Reference](#reference) + * [`bitser.dumps`](#dumps) + * [`bitser.dumpLoveFile`](#dumplovefile) + * [`bitser.loads`](#loads) + * [`bitser.loadData`](#loaddata) + * [`bitser.loadLoveFile`](#loadlovefile) + * [`bitser.register`](#register) + * [`bitser.registerClass`](#registerclass) + * [`bitser.unregister`](#unregister) + * [`bitser.unregisterClass`](#unregisterclass) + * [`bitser.reserveBuffer`](#reservebuffer) + * [`bitser.clearBuffer`](#clearbuffer) + +# Basic usage + +```lua +local bitser = require 'bitser' + +-- some_thing can be almost any lua value +local binary_data = bitser.dumps(some_thing) + +-- binary_data is a string containing some serialized value +local copy_of_some_thing = bitser.loads(binary_data) +``` + +Bitser can't dump values of type `function`, `userdata` or `thread`, or anything that +contains one of those. If you need to, look into [`bitser.register`](#register). + +# Serializing class instances + +All you need to make bitser correctly serialize your class instances is register that class: + +```lua +-- this is usually enough +bitser.registerClass(MyClass) + +-- if you use Slither, you can add it to __attributes__ +class 'MyClass' { + __attributes__ = {bitser.registerClass}, + -- insert rest of class here +} + +local data = bitser.dumps(MyClass(42)) +local instance = bitser.loads(data) +``` + +Note that classnames need to be unique to avoid confusion, so if you have two different classes named `Foo` you'll need to do +something like: + +```lua +-- in module_a.lua +bitser.registerClass('module_a.Foo', Foo) + +-- in module_b.lua +bitser.registerClass('module_b.Foo', Foo) +``` + +See the reference sections on [`bitser.registerClass`](#registerclass) and +[`bitser.unregisterClass`](#unregisterclass) for more information. + +## Supported class libraries + +* MiddleClass +* SECL +* hump.class +* Slither +* Moonscript classes + +# Advanced usage + +If you use [LÖVE](https://love2d.org/), you'll want to use [`bitser.dumpLoveFile`](#dumplovefile) and [`bitser.loadLoveFile`](#loadlovefile) if you want to serialize to the save directory. You also might have images and other resources that you'll need to register, like follows: + +```lua +function love.load() + bad_guy_img = bitser.register('bad_guy_img', love.graphics.newImage('img/bad_guy.png')) + if love.filesystem.exists('save_point.dat') then + level_data = bitser.loadLoveFile('save_point.dat') + else + level_data = create_level_data() + end +end + +function save_point_reached() + bitser.dumpLoveFile('save_point.dat', level_data) +end +``` + +# Reference + +## dumps + +```lua +string = bitser.dumps(value) +``` + +Basic serialization of `value` into a Lua string. + +See also: [`bitser.loads`](#loads). + +## dumpLoveFile + +```lua +bitser.dumpLoveFile(file_name, value) +``` + +Serializes `value` and writes the result to `file_name` more efficiently than serializing to a string and writing +that string to a file. Only useful if you're running [LÖVE](https://love2d.org/). + +See also: [`bitser.loadLoveFile`](#loadlovefile). + +## loads + +```lua +value = bitser.loads(string) +``` + +Deserializes `value` from `string`. + +See also: [`bitser.dumps`](#dumps). + +## loadData + +```lua +value = bitser.loadData(light_userdata, size) +``` + +Deserializes `value` from raw data. You probably won't need to use this function ever. + +When running [LÖVE](https://love2d.org/), you would use it like this: + +```lua +value = bitser.loadData(data:getPointer(), data:getSize()) +``` + +Where `data` is an instance of a subclass of [Data](https://love2d.org/wiki/Data). + +## loadLoveFile + +```lua +value = bitser.loadLoveFile(file_name) +``` + +Reads from `file_name` and deserializes `value` more efficiently than reading the file and then deserializing that string. +Only useful if you're running [LÖVE](https://love2d.org/). + +See also: [`bitser.dumpLoveFile`](#dumplovefile). + +## register + +```lua +resource = bitser.register(name, resource) +``` + +Registers the value `resource` with the name `name`, which has to be a unique string. Registering static resources like images, +functions, classes and huge strings, makes sure bitser doesn't attempt to serialize them, but only stores a named +reference to them. + +Returns the registered resource as a convenience. + +See also: [`bitser.unregister`](#unregister). + +## registerClass + +```lua +class = bitser.registerClass(class) +class = bitser.registerClass(name, class) +class = bitser.registerClass(name, class, classkey, deserializer) +``` + +Registers the class `class`, so that bitser can correctly serialize and deserialize instances of `class`. + +Note that if you want to serialize the class _itself_, you'll need to [register the class as a resource](#register). + +Most of the time the first variant is enough, but some class libraries don't store the +class name on the class object itself, in which case you'll need to use the second variant. + +Class names also have to be unique, so if you use multiple classes with the same name, you'll need to use the second +variant as well to give them different names. + +The arguments `classkey` and `deserializer` exist so you can hook in unsupported class libraries without needing +to patch bitser. [See the list of supported class libraries](#supported-class-libraries). + +If not nil, the argument `classkey` should be a string such that +`rawget(obj, classkey) == class` for any `obj` whose type is `class`. This is done so that key is skipped for serialization. + +If not nil, the argument `deserializer` should be a function such that `deserializer(obj, class)` returns a valid +instance of `class` with the properties of `obj`. `deserializer` is allowed to mutate `obj`. + +Returns the registered resource as a convenience. + +See also: [`bitser.unregisterClass`](#unregisterclass). + +## unregister + +```lua +bitser.unregister(name) +``` + +Deregisters the previously registered value with the name `name`. + +See also: [`bitser.register`](#register). + +## unregisterClass + +```lua +bitser.unregisterClass(name) +``` + +Deregisters the previously registered class with the name `name`. Note that this works by name and not value, +which is useful in a context where you don't have a reference to the class you want to unregister. + +See also: [`bitser.registerClass`](#registerclass). + +## reserveBuffer + +```lua +bitser.reserveBuffer(num_bytes) +``` + +Makes sure the buffer used for reading and writing serialized data is at least `num_bytes` large. +You probably don't need to ever use this function. + +## clearBuffer + +```lua +bitser.clearBuffer() +``` + +Frees up the buffer used for reading and writing serialized data for garbage collection. +You'll rarely need to use this function, except if you needed a huge buffer before and now only need a small buffer +(or are done (de)serializing altogether). Most of the time, using this function will decrease performance needlessly. diff --git a/src/lib/bitser/bitser.lua b/src/lib/bitser/bitser.lua new file mode 100644 index 0000000..4e05842 --- /dev/null +++ b/src/lib/bitser/bitser.lua @@ -0,0 +1,432 @@ +--[[ +Copyright (c) 2016, Robin Wellner + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +]] + +local floor = math.floor +local pairs = pairs +local type = type +local insert = table.insert +local getmetatable = getmetatable +local setmetatable = setmetatable + +local ffi = require("ffi") +local buf_pos = 0 +local buf_size = -1 +local buf = nil +local writable_buf = nil +local writable_buf_size = nil + +local function Buffer_prereserve(min_size) + if buf_size < min_size then + buf_size = min_size + buf = ffi.new("uint8_t[?]", buf_size) + end +end + +local function Buffer_clear() + buf_size = -1 + buf = nil + writable_buf = nil + writable_buf_size = nil +end + +local function Buffer_makeBuffer(size) + if writable_buf then + buf = writable_buf + buf_size = writable_buf_size + writable_buf = nil + writable_buf_size = nil + end + buf_pos = 0 + Buffer_prereserve(size) +end + +local function Buffer_newReader(str) + Buffer_makeBuffer(#str) + ffi.copy(buf, str, #str) +end + +local function Buffer_newDataReader(data, size) + writable_buf = buf + writable_buf_size = buf_size + buf_pos = 0 + buf_size = size + buf = ffi.cast("uint8_t*", data) +end + +local function Buffer_reserve(additional_size) + while buf_pos + additional_size > buf_size do + buf_size = buf_size * 2 + local oldbuf = buf + buf = ffi.new("uint8_t[?]", buf_size) + ffi.copy(buf, oldbuf, buf_pos) + end +end + +local function Buffer_write_byte(x) + Buffer_reserve(1) + buf[buf_pos] = x + buf_pos = buf_pos + 1 +end + +local function Buffer_write_string(s) + Buffer_reserve(#s) + ffi.copy(buf + buf_pos, s, #s) + buf_pos = buf_pos + #s +end + +local function Buffer_write_data(ct, len, ...) + Buffer_reserve(len) + ffi.copy(buf + buf_pos, ffi.new(ct, ...), len) + buf_pos = buf_pos + len +end + +local function Buffer_ensure(numbytes) + if buf_pos + numbytes > buf_size then + error("malformed serialized data") + end +end + +local function Buffer_read_byte() + Buffer_ensure(1) + local x = buf[buf_pos] + buf_pos = buf_pos + 1 + return x +end + +local function Buffer_read_string(len) + Buffer_ensure(len) + local x = ffi.string(buf + buf_pos, len) + buf_pos = buf_pos + len + return x +end + +local function Buffer_read_data(ct, len) + Buffer_ensure(len) + local x = ffi.new(ct) + ffi.copy(x, buf + buf_pos, len) + buf_pos = buf_pos + len + return x +end + +local resource_registry = {} +local resource_name_registry = {} +local class_registry = {} +local class_name_registry = {} +local classkey_registry = {} +local class_deserialize_registry = {} + +local serialize_value + +local function write_number(value, _) + if floor(value) == value and value >= -2147483648 and value <= 2147483647 then + if value >= -27 and value <= 100 then + --small int + Buffer_write_byte(value + 27) + elseif value >= -32768 and value <= 32767 then + --short int + Buffer_write_byte(250) + Buffer_write_data("int16_t[1]", 2, value) + else + --long int + Buffer_write_byte(245) + Buffer_write_data("int32_t[1]", 4, value) + end + else + --double + Buffer_write_byte(246) + Buffer_write_data("double[1]", 8, value) + end +end + +local function write_string(value, _) + if #value < 32 then + --short string + Buffer_write_byte(192 + #value) + else + --long string + Buffer_write_byte(244) + write_number(#value) + end + Buffer_write_string(value) +end + +local function write_nil(_, _) + Buffer_write_byte(247) +end + +local function write_boolean(value, _) + Buffer_write_byte(value and 249 or 248) +end + +local function write_table(value, seen) + local classkey + local classname = (class_name_registry[value.class] -- MiddleClass + or class_name_registry[value.__baseclass] -- SECL + or class_name_registry[getmetatable(value)] -- hump.class + or class_name_registry[value.__class__] -- Slither + or class_name_registry[value.__class]) -- Moonscript class + if classname then + classkey = classkey_registry[classname] + Buffer_write_byte(242) + serialize_value(classname, seen) + else + Buffer_write_byte(240) + end + local len = #value + write_number(len, seen) + for i = 1, len do + serialize_value(value[i], seen) + end + local klen = 0 + for k in pairs(value) do + if (type(k) ~= 'number' or floor(k) ~= k or k > len or k < 1) and k ~= classkey then + klen = klen + 1 + end + end + write_number(klen, seen) + for k, v in pairs(value) do + if (type(k) ~= 'number' or floor(k) ~= k or k > len or k < 1) and k ~= classkey then + serialize_value(k, seen) + serialize_value(v, seen) + end + end +end + +local types = {number = write_number, string = write_string, table = write_table, boolean = write_boolean, ["nil"] = write_nil} + +serialize_value = function(value, seen) + if seen[value] then + local ref = seen[value] + if ref < 64 then + --small reference + Buffer_write_byte(128 + ref) + else + --long reference + Buffer_write_byte(243) + write_number(ref, seen) + end + return + end + local t = type(value) + if t ~= 'number' and t ~= 'boolean' and t ~= 'nil' then + seen[value] = seen.len + seen.len = seen.len + 1 + end + if resource_name_registry[value] then + local name = resource_name_registry[value] + if #name < 16 then + --small resource + Buffer_write_byte(224 + #name) + Buffer_write_string(name) + else + --long resource + Buffer_write_byte(241) + write_string(name, seen) + end + return + end + (types[t] or + error("cannot serialize type " .. t) + )(value, seen) +end + +local function serialize(value) + Buffer_makeBuffer(4096) + local seen = {len = 0} + serialize_value(value, seen) +end + +local function add_to_seen(value, seen) + insert(seen, value) + return value +end + +local function reserve_seen(seen) + insert(seen, 42) + return #seen +end + +local function deserialize_value(seen) + local t = Buffer_read_byte() + if t < 128 then + --small int + return t - 27 + elseif t < 192 then + --small reference + return seen[t - 127] + elseif t < 224 then + --small string + return add_to_seen(Buffer_read_string(t - 192), seen) + elseif t < 240 then + --small resource + return add_to_seen(resource_registry[Buffer_read_string(t - 224)], seen) + elseif t == 240 then + --table + local v = add_to_seen({}, seen) + local len = deserialize_value(seen) + for i = 1, len do + v[i] = deserialize_value(seen) + end + len = deserialize_value(seen) + for _ = 1, len do + local key = deserialize_value(seen) + v[key] = deserialize_value(seen) + end + return v + elseif t == 241 then + --long resource + local idx = reserve_seen(seen) + local value = resource_registry[deserialize_value(seen)] + seen[idx] = value + return value + elseif t == 242 then + --instance + local instance = add_to_seen({}, seen) + local classname = deserialize_value(seen) + local class = class_registry[classname] + local classkey = classkey_registry[classname] + local deserializer = class_deserialize_registry[classname] + local len = deserialize_value(seen) + for i = 1, len do + instance[i] = deserialize_value(seen) + end + len = deserialize_value(seen) + for _ = 1, len do + local key = deserialize_value(seen) + instance[key] = deserialize_value(seen) + end + if classkey then + instance[classkey] = class + end + return deserializer(instance, class) + elseif t == 243 then + --reference + return seen[deserialize_value(seen) + 1] + elseif t == 244 then + --long string + return add_to_seen(Buffer_read_string(deserialize_value(seen)), seen) + elseif t == 245 then + --long int + return Buffer_read_data("int32_t[1]", 4)[0] + elseif t == 246 then + --double + return Buffer_read_data("double[1]", 8)[0] + elseif t == 247 then + --nil + return nil + elseif t == 248 then + --false + return false + elseif t == 249 then + --true + return true + elseif t == 250 then + --short int + return Buffer_read_data("int16_t[1]", 2)[0] + else + error("unsupported serialized type " .. t) + end +end + +local function deserialize_MiddleClass(instance, class) + return setmetatable(instance, class.__instanceDict) +end + +local function deserialize_SECL(instance, class) + return setmetatable(instance, getmetatable(class)) +end + +local deserialize_humpclass = setmetatable + +local function deserialize_Slither(instance, class) + return getmetatable(class).allocate(instance) +end + +local function deserialize_Moonscript(instance, class) + return setmetatable(instance, class.__base) +end + +return {dumps = function(value) + serialize(value) + return ffi.string(buf, buf_pos) +end, dumpLoveFile = function(fname, value) + serialize(value) + love.filesystem.write(fname, ffi.string(buf, buf_pos)) +end, loadLoveFile = function(fname) + local serializedData = love.filesystem.newFileData(fname) + Buffer_newDataReader(serializedData:getPointer(), serializedData:getSize()) + return deserialize_value({}) +end, loadData = function(data, size) + Buffer_newDataReader(data, size) + return deserialize_value({}) +end, loads = function(str) + Buffer_newReader(str) + return deserialize_value({}) +end, register = function(name, resource) + assert(not resource_registry[name], name .. " already registered") + resource_registry[name] = resource + resource_name_registry[resource] = name + return resource +end, unregister = function(name) + resource_name_registry[resource_registry[name]] = nil + resource_registry[name] = nil +end, registerClass = function(name, class, classkey, deserializer) + if not class then + class = name + name = class.__name__ or class.name or class.__name + end + if not classkey then + if class.__instanceDict then + -- assume MiddleClass + classkey = 'class' + elseif class.__baseclass then + -- assume SECL + classkey = '__baseclass' + end + -- assume hump.class, Slither, Moonscript class or something else that doesn't store the + -- class directly on the instance + end + if not deserializer then + if class.__instanceDict then + -- assume MiddleClass + deserializer = deserialize_MiddleClass + elseif class.__baseclass then + -- assume SECL + deserializer = deserialize_SECL + elseif class.__index == class then + -- assume hump.class + deserializer = deserialize_humpclass + elseif class.__name__ then + -- assume Slither + deserializer = deserialize_Slither + elseif class.__base then + -- assume Moonscript class + deserializer = deserialize_Moonscript + else + error("no deserializer given for unsupported class library") + end + end + class_registry[name] = class + classkey_registry[name] = classkey + class_deserialize_registry[name] = deserializer + class_name_registry[class] = name + return class +end, unregisterClass = function(name) + class_name_registry[class_registry[name]] = nil + classkey_registry[name] = nil + class_deserialize_registry[name] = nil + class_registry[name] = nil +end, reserveBuffer = Buffer_prereserve, clearBuffer = Buffer_clear} diff --git a/src/lib/bitser/cases/_new.lua b/src/lib/bitser/cases/_new.lua new file mode 100644 index 0000000..3ef06c3 --- /dev/null +++ b/src/lib/bitser/cases/_new.lua @@ -0,0 +1,3 @@ +-- write your own! +-- data to be tested, repetitions, number of tries +return {}, 10000, 3 \ No newline at end of file diff --git a/src/lib/bitser/cases/bigtable.lua b/src/lib/bitser/cases/bigtable.lua new file mode 100644 index 0000000..45e07c2 --- /dev/null +++ b/src/lib/bitser/cases/bigtable.lua @@ -0,0 +1,7 @@ +local t = {} + +for i = 1, 2000 do + t[i] = 100 +end + +return t, 500, 5 \ No newline at end of file diff --git a/src/lib/bitser/cases/cthulhu.lua b/src/lib/bitser/cases/cthulhu.lua new file mode 100644 index 0000000..179b89d --- /dev/null +++ b/src/lib/bitser/cases/cthulhu.lua @@ -0,0 +1,7 @@ +local cthulhu = {{}, {}, {}} +cthulhu.fhtagn = cthulhu +cthulhu[1][cthulhu[2]] = cthulhu[3] +cthulhu[2][cthulhu[1]] = cthulhu[2] +cthulhu[3][cthulhu[3]] = cthulhu + +return cthulhu, 10000, 3 \ No newline at end of file diff --git a/src/lib/bitser/cases/intkeys.lua b/src/lib/bitser/cases/intkeys.lua new file mode 100644 index 0000000..bcfb1ef --- /dev/null +++ b/src/lib/bitser/cases/intkeys.lua @@ -0,0 +1,7 @@ +local t = {} + +for i = 1, 200 do + t[math.random(1000)] = math.random(100) +end + +return t, 30000, 5 \ No newline at end of file diff --git a/src/lib/bitser/cases/shared_table.lua b/src/lib/bitser/cases/shared_table.lua new file mode 100644 index 0000000..429b64b --- /dev/null +++ b/src/lib/bitser/cases/shared_table.lua @@ -0,0 +1,6 @@ +local t = {} +local x = {10, 50, 40, 30, 20} +for i = 1, 40 do + t[i] = x +end +return t, 10000, 3 \ No newline at end of file diff --git a/src/lib/bitser/conf.lua b/src/lib/bitser/conf.lua new file mode 100644 index 0000000..9b1f427 --- /dev/null +++ b/src/lib/bitser/conf.lua @@ -0,0 +1,4 @@ +function love.conf(t) + t.version = "0.10.0" + t.console = true +end \ No newline at end of file diff --git a/src/lib/bitser/main.lua b/src/lib/bitser/main.lua new file mode 100644 index 0000000..2c5a35d --- /dev/null +++ b/src/lib/bitser/main.lua @@ -0,0 +1,183 @@ +local found_bitser, bitser = pcall(require, 'bitser') +local found_binser, binser = pcall(require, 'binser') +local found_ser, ser = pcall(require, 'ser') +local found_serpent, serpent = pcall(require, 'serpent') +local found_smallfolk, smallfolk = pcall(require, 'smallfolk') +local found_msgpack, msgpack = pcall(require, 'MessagePack') + +local cases +local selected_case = 1 + +local sers = {} +local desers = {} + +if found_bitser then + sers.bitser = bitser.dumps + desers.bitser = bitser.loads + bitser.reserveBuffer(1024 * 1024) +end + +if found_binser then + sers.binser = binser.s + desers.binser = binser.d +end + +if found_ser then + sers.ser = ser + desers.ser = loadstring +end + +if found_serpent then + sers.serpent = serpent.dump + desers.serpent = loadstring +end + +if found_smallfolk then + sers.smallfolk = smallfolk.dumps + desers.smallfolk = smallfolk.loads +end + +if found_msgpack then + sers.msgpack = msgpack.pack + desers.msgpack = msgpack.unpack +end + +local view_absolute = true +local resultname = "serialisation time in seconds" + +function love.load() + cases = love.filesystem.getDirectoryItems("cases") + state = 'select_case' + love.graphics.setBackgroundColor(255, 230, 220) + love.graphics.setColor(40, 30, 0) + love.window.setTitle("Select a benchmark testcase") +end + +function love.keypressed(key) + if state == 'select_case' then + if key == 'up' then + selected_case = (selected_case - 2) % #cases + 1 + elseif key == 'down' then + selected_case = selected_case % #cases + 1 + elseif key == 'return' then + state = 'calculate_results' + love.window.setTitle("Running benchmark...") + end + elseif state == 'results' then + if key == 'r' then + view_absolute = not view_absolute + elseif key == 'right' then + if results == results_ser then + results = results_deser + resultname = "deserialisation time in seconds" + elseif results == results_deser then + results = results_size + resultname = "size of output in bytes" + elseif results == results_size then + results = results_ser + resultname = "serialisation time in seconds" + end + elseif key == 'left' then + if results == results_ser then + results = results_size + resultname = "size of output in bytes" + elseif results == results_deser then + results = results_ser + resultname = "serialisation time in seconds" + elseif results == results_size then + results = results_deser + resultname = "deserialisation time in seconds" + end + elseif key == 'escape' then + state = 'select_case' + love.window.setTitle("Select a benchmark testcase") + end + end +end + +function love.draw() + if state == 'select_case' then + for i, case in ipairs(cases) do + love.graphics.print(case, selected_case == i and 60 or 20, i * 20) + end + elseif state == 'calculate_results' then + love.graphics.print("Running benchmark...", 20, 20) + love.graphics.print("This may take a while", 20, 40) + state = 'calculate_results_2' + elseif state == 'calculate_results_2' then + local data, iters, tries = love.filesystem.load("cases/" .. cases[selected_case])() + results_ser = {} + results = results_ser + resultname = "serialisation time in seconds" + results_size = {} + results_deser = {} + for sername, serializer in pairs(sers) do + results_ser[sername] = math.huge + results_deser[sername] = math.huge + end + local outputs = {} + for try = 1, tries do + for sername, serializer in pairs(sers) do + local output + local success, diff = pcall(function() + local t = os.clock() + for i = 1, iters do + output = serializer(data) + end + return os.clock() - t + end) + if success and diff < results_ser[sername] then + results_ser[sername] = diff + end + if try == 1 then + outputs[sername] = output + results_size[sername] = output and #output or math.huge + end + end + end + for try = 1, tries do + for sername, deserializer in pairs(desers) do + local input = outputs[sername] + local success, diff = pcall(function() + local t = os.clock() + for i = 1, iters / 10 do + deserializer(input) + end + return os.clock() - t + end) + if success and diff < results_deser[sername] then + results_deser[sername] = diff + end + end + end + state = 'results' + love.window.setTitle("Results for " .. cases[selected_case]) + elseif state == 'results' then + local results_min = math.huge + local results_max = -math.huge + for sername, result in pairs(results) do + if result < results_min then + results_min = result + end + if result > results_max and result < math.huge then + results_max = result + end + end + if view_absolute then results_min = 0 end + local i = 1 + for sername, result in pairs(results) do + love.graphics.print(sername, 20, i * 20) + if result == math.huge then + love.graphics.setColor(220, 30, 0) + love.graphics.rectangle('fill', 100, i * 20, 780 - 100, 18) + love.graphics.setColor(40, 30, 0) + else + love.graphics.rectangle('fill', 100, i * 20, (780 - 100) * (result - results_min) / (results_max - results_min), 18) + end + i = i + 1 + end + love.graphics.print(results_min, 100, i * 20) + love.graphics.print(results_max, 780 - love.graphics.getFont():getWidth(results_max), i * 20) + love.graphics.print(resultname .." (smaller is better; try left, right, R, escape)", 100, i * 20 + 20) + end +end \ No newline at end of file diff --git a/src/lib/bitser/spec/bitser_spec.lua b/src/lib/bitser/spec/bitser_spec.lua new file mode 100644 index 0000000..ce5d8a4 --- /dev/null +++ b/src/lib/bitser/spec/bitser_spec.lua @@ -0,0 +1,297 @@ +local ffi = require 'ffi' + +_G.love = {filesystem = {newFileData = function() + return {getPointer = function() + local buf = ffi.new("uint8_t[?]", #love.s) + ffi.copy(buf, love.s, #love.s) + return buf + end, getSize = function() + return #love.s + end} +end, write = function(_, s) + love.s = s +end}} + +local bitser = require 'bitser' + +local function serdeser(value) + return bitser.loads(bitser.dumps(value)) +end + +local function test_serdeser(value) + assert.are.same(serdeser(value), value) +end + +describe("bitser", function() + it("serializes simple values", function() + test_serdeser(true) + test_serdeser(false) + test_serdeser(nil) + test_serdeser(1) + test_serdeser(-1) + test_serdeser(0) + test_serdeser(100000000) + test_serdeser(1.234) + test_serdeser(10 ^ 20) + test_serdeser(1/0) + test_serdeser(-1/0) + test_serdeser("") + test_serdeser("hullo") + test_serdeser([[this + is a longer string + such a long string + that it won't fit + in the "short string" representation + no it won't + listen to me + it won't]]) + local nan = serdeser(0/0) + assert.is_not.equal(nan, nan) + end) + it("serializes simple tables", function() + test_serdeser({}) + test_serdeser({10, 11, 12}) + test_serdeser({foo = 10, bar = 99, [true] = false}) + test_serdeser({[1000] = 9000}) + test_serdeser({{}}) + end) + it("serializes tables with tables as keys", function() + local thekey = {"Heyo"} + assert.are.same(thekey, (next(serdeser({[thekey] = 12})))) + end) + it("serializes cyclic tables", function() + local cthulhu = {{}, {}, {}} + cthulhu.fhtagn = cthulhu + --note: this does not test tables as keys because assert.are.same doesn't like that + cthulhu[1].cthulhu = cthulhu[3] + cthulhu[2].cthulhu = cthulhu[2] + cthulhu[3].cthulhu = cthulhu + + test_serdeser(cthulhu) + end) + it("serializes resources", function() + local temp_resource = {} + bitser.register("temp_resource", temp_resource) + assert.are.equal(serdeser({this = temp_resource}).this, temp_resource) + bitser.unregister("temp_resource") + end) + it("serializes many resources", function() + local max = 1000 + local t = {} + for i = 1, max do + bitser.register(tostring(i), i) + t[i] = i + end + test_serdeser(t) + for i = 1, max do + bitser.unregister(tostring(i)) + end + end) + it("serializes deeply nested tables", function() + local max = 1000 + local t = {} + for _ = 1, max do + t.t = {} + t = t.t + end + test_serdeser(t) + end) + it("serializes MiddleClass instances", function() + local class = require("middleclass") + local Horse = bitser.registerClass(class('Horse')) + function Horse:initialize(name) + self.name = name + self[1] = 'instance can be sequence' + end + local bojack = Horse('Bojack Horseman') + test_serdeser(bojack) + assert.is_true(serdeser(bojack):isInstanceOf(Horse)) + bitser.unregisterClass('Horse') + end) + it("serializes SECL instances", function() + local class_mt = {} + + function class_mt:__index(key) + return self.__baseclass[key] + end + + local class = setmetatable({ __baseclass = {} }, class_mt) + + function class:new(...) + local c = {} + c.__baseclass = self + setmetatable(c, getmetatable(self)) + if c.init then + c:init(...) + end + return c + end + + local Horse = bitser.registerClass('Horse', class:new()) + function Horse:init(name) + self.name = name + self[1] = 'instance can be sequence' + end + local bojack = Horse:new('Bojack Horseman') + test_serdeser(bojack) + assert.are.equal(serdeser(bojack).__baseclass, Horse) + bitser.unregisterClass('Horse') + end) + it("serializes hump.class instances", function() + local class = require("class") + local Horse = bitser.registerClass('Horse', class{}) + function Horse:init(name) + self.name = name + self[1] = 'instance can be sequence' + end + local bojack = Horse('Bojack Horseman') + test_serdeser(bojack) + assert.are.equal(getmetatable(serdeser(bojack)), Horse) + bitser.unregisterClass('Horse') + end) + it("serializes Slither instances", function() + local class = require("slither") + local Horse = class.private 'Horse' { + __attributes__ = {bitser.registerClass}, + __init__ = function(self, name) + self.name = name + self[1] = 'instance can be sequence' + end + } + local bojack = Horse('Bojack Horseman') + test_serdeser(bojack) + assert.is_true(class.isinstance(serdeser(bojack), Horse)) + bitser.unregisterClass('Horse') + end) + it("serializes Moonscript class instances", function() + local Horse + do + local _class_0 + local _base_0 = {} + _base_0.__index = _base_0 + _class_0 = setmetatable({ + __init = function(self, name) + self.name = name + self[1] = 'instance can be sequence' + end, + __base = _base_0, + __name = "Horse"}, { + __index = _base_0, + __call = function(cls, ...) + local _self_0 = setmetatable({}, _base_0) + cls.__init(_self_0, ...) + return _self_0 + end + }) + _base_0.__class = _class_0 + Horse = _class_0 + end + assert.are.same(Horse.__name, "Horse") -- to shut coveralls up + bitser.registerClass(Horse) + local bojack = Horse('Bojack Horseman') + test_serdeser(bojack) + local new_bojack = serdeser(bojack) + assert.are.equal(new_bojack.__class, Horse) + bitser.unregisterClass('Horse') + end) + it("serializes custom class instances", function() + local Horse_mt = bitser.registerClass('Horse', {__index = {}}, nil, setmetatable) + local function Horse(name) + local self = {} + self.name = name + self[1] = 'instance can be sequence' + return setmetatable(self, Horse_mt) + end + local bojack = Horse('Bojack Horseman') + test_serdeser(bojack) + assert.are.equal(getmetatable(serdeser(bojack)), Horse_mt) + bitser.unregisterClass('Horse') + end) + it("serializes classes that repeat keys", function() + local my_mt = {"hi"} + local works = { foo = 'a', bar = {baz = 'b'}, } + local broken = { foo = 'a', bar = {foo = 'b'}, } + local more_broken = { + foo = 'a', + baz = {foo = 'b', bar = 'c'}, + quz = {bar = 'd', bam = 'e'} + } + setmetatable(works, my_mt) + setmetatable(broken, my_mt) + setmetatable(more_broken, my_mt) + bitser.registerClass("Horse", my_mt, nil, setmetatable) + test_serdeser(works) + test_serdeser(broken) + test_serdeser(more_broken) + bitser.unregisterClass('Horse') + end) + it("serializes big data", function() + local text = "this is a lot of nonsense, please disregard, we need a lot of data to get past 4 KiB (114 characters should do it)" + local t = {} + for i = 1, 40 do + t[i] = text .. i -- no references allowed! + end + test_serdeser(t) + end) + it("serializes many references", function() + local max = 1000 + local t = {} + local t2 = {} + for i = 1, max do + t.t = {} + t = t.t + t2[i] = t + end + test_serdeser({t, t2}) + end) + it("serializes resources with long names", function() + local temp_resource = {} + bitser.register("temp_resource_or_whatever", temp_resource) + assert.are.equal(serdeser({this = temp_resource}).this, temp_resource) + bitser.unregister("temp_resource_or_whatever") + end) + it("serializes resources with the same name as serialized strings", function() + local temp_resource = {} + bitser.register('temp', temp_resource) + test_serdeser({temp='temp', {temp_resource}}) + bitser.unregister('temp') + end) + it("serializes resources with the same long name as serialized strings", function() + local temp_resource = {} + bitser.register('temp_resource_or_whatever', temp_resource) + test_serdeser({temp_resource_or_whatever='temp_resource_or_whatever', {temp_resource}}) + bitser.unregister('temp_resource_or_whatever') + end) + it("cannot serialize functions", function() + assert.has_error(function() bitser.dumps(function() end) end, "cannot serialize type function") + end) + it("cannot serialize unsupported class libraries without explicit deserializer", function() + assert.has_error(function() bitser.registerClass('Horse', {mane = 'majestic'}) end, "no deserializer given for unsupported class library") + end) + it("cannot deserialize values from unassigned type bytes", function() + assert.has_error(function() bitser.loads("\251") end, "unsupported serialized type 251") + assert.has_error(function() bitser.loads("\252") end, "unsupported serialized type 252") + assert.has_error(function() bitser.loads("\253") end, "unsupported serialized type 253") + assert.has_error(function() bitser.loads("\254") end, "unsupported serialized type 254") + assert.has_error(function() bitser.loads("\255") end, "unsupported serialized type 255") + end) + it("can load from raw data", function() + assert.are.same(bitser.loadData(ffi.new("uint8_t[4]", 195, 103, 118, 120), 4), "gvx") + end) + it("will not read past the end of the buffer", function() + assert.has_error(function() bitser.loadData(ffi.new("uint8_t[4]", 196, 103, 118, 120), 4) end) + end) + it("can clear the buffer", function() + bitser.clearBuffer() + end) + it("can write to new buffer after reading from read-only buffer", function() + test_serdeser("bitser") + bitser.loadData(ffi.new("uint8_t[4]", 195, 103, 118, 120), 4) + test_serdeser("bitser") + end) + it("it can dump and load LÖVE files", function() + local v = {value = "value"} + bitser.dumpLoveFile("some_file_name", v) + assert.are.same(v, bitser.loadLoveFile("some_file_name")) + end) +end)