Giter Club home page Giter Club logo

binser's Introduction

binser - Customizable Lua Serializer

Build Status

There already exists a number of serializers for Lua, each with their own uses, limitations, and quirks. binser is yet another robust, pure Lua serializer that specializes in serializing Lua data with lots of userdata and custom classes and types. binser is a binary serializer and does not serialize data into human readable representation or use the Lua parser to read expressions. This makes it safe and moderately fast, especially on LuaJIT. binser also handles cycles, self-references, and metatables.

How to Use

Example

local binser = require "binser"

local mydata = binser.serialize(45, {4, 8, 12, 16}, "Hello, World!")

print(binser.deserializeN(mydata, 3))
-- 45	table: 0x7fa60054bdb0	Hello, World!

Serializing and Deserializing

local str = binser.serialize(...)

Serialize (almost) any Lua data into a Lua string. Numbers, strings, tables, booleans, and nil are all fully supported by default. Custom userdata and custom types, both identified by metatables, can also be supported by specifying a custom serialization function. Unserializable data should throw an error. Aliased to binser.s.

local results, len = binser.deserialize(str[, index])

Deserialize any string previously serialized by binser. Can optionally start at an index in the string (to drop leading characters). Index is 1 by default. Unrecognized data should throw an error. Results is a list of length len. Aliased to binser.d.

local ... = binser.deserializeN(str, n[, index])

Deserializes at most n values from str. The default value for n is one, so binser.deserializeN(str) will deserialize exactly one value from string, and ignore the rest of the string. Can optionally start at a given index, which is 1 by default. Aliased to binser.dn.

Custom types

local metatable = binser.register(metatable, name, serialize, deserialize)

Registers a custom type, identified by its metatable, to be serialized. Registering types has two main purposes. First, it allows custom serialization and deserialization for userdata and tables that contain userdata, which can't otherwise be serialized in a uniform way. Second, it allows efficient serialization of small tables with large metatables, as registered metatables are not serialized.

The metatable parameter is the metatable the identifies the type. The name parameter is the type name used in serialization. The only requirement for names is that they are unique. The serialize and deserialize parameters are a pair of functions that construct and destruct and instance of the type. serialize can return any number of serializable Lua objects, and deserialize should accept the arguments returned by serialize. serialize and deserialize can also be specified in metatable._serialize and metatable._deserialize respectively.

If serialize and deserialize are omitted, then default table serializers are used, which work very well for most tables. If your type describes userdata, however, serialize and deserialize must be provided.

local class = binser.registerClass(class[, name])

Registers a class as a custom type. binser currently supports 30log and middleclass. name is an optional parameter that defaults to class.name.

local metatable = binser.unregister(name)

Users should seldom need this, but to explicitly unregister a type, call this.

Templates

If binser's already compact serialization isn't enough, and you don't want to write complex and error prone custom serializers, binser has a functionality called templating. Templates specify the layout of a custom type, so that table keys don't need to be serialized many times. To specify a template, add the _template key to the metatable of your type.

An example:

local template = {
	"name", "age", "salary", "email",
	nested = {"more", "nested", "keys"}
}

local Employee_MT = {
	name = "Employee",
}

local joe = setmetatable({
	name = "Joe",
	age = 11,
	salary = "$1,000,000",
	email = "[email protected]",
	nested = {
		more = "blah",
		nested = "FUBAR",
		keys = "lost"
	}
}, Employee_MT)

-- Print length of serialized employee without templating
-- 117
binser.registerClass(Employee_MT)
print(#binser.s(joe))
binser.unregister(Employee_MT)

-- Print length of serialized employee with templating
-- 72
Employee_MT._template = template
binser.registerClass(Employee_MT)
print(#binser.s(joe))

In the above example, the resulting serialized value with templating is nearly half of the size of the default table serialization.

Resources

If there are certain objects that don't need to be serialized at all, like images, audio, or any system resource, binser can mark them as such to only serialize a reference to them. Resources must be registered in a similar way to custom types and given a unique name.

local resource = binser.registerResource(resource, name)

Registers a resource.

local resource = binser.unregisterResource(name)

Resources can be unregistered in a similar manner as custom types.

File IO

Mostly for convenience, binser has functions for writing and reading to files. These work through Lua's built in IO.

binser.writeFile(filepath, ...)

Serializes Lua objects and writes them to a file. Overwrites the previous file.

binser.appendFile(filepath, ...)

Same as writing to a file, but doesn't overwrite the old file.

local results, len = binser.readFile(filepath)

Reads and deserializes a file.

The trio of file convenience function have shortened aliases as well.

Function Alias
binser.writeFile binser.w
binser.appendFile binser.a
binser.readFile binser.r

Why

Most Lua serializers serialize into valid Lua code, which while very useful, makes it impossible to do things like custom serialization and deserialization. binser was originally written as a way to save game levels with images and other native resources, but is extremely general.

LuaRocks

binser is available as a rock on LuaRocks. Install via:

luarocks install binser

Testing

binser uses busted for testing. Install and run busted from the command line to test.

Notes

  • Serialized strings can contain unprintable and null characters.
  • Serialized data can be appended to other serialized data. (Cool :))
  • The functions binser.serialize, binser.deserialize, and binser.deserializeN can be shortened to binser.s, binser.d, and binser.dn as handy shortcuts.

Bugs

Pull requests are welcome, please help me squash bugs!

binser's People

Contributors

bakpakin avatar danggui1995 avatar osch avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

binser's Issues

readFile and writeFile do not return an error

Lua 5.4.0 on Arch Linux

Current readPath and writePath follow a pattern that if the file does not exist it wraps the io.open return value in an assert which then calls Lua's error stack. In one instance this caused me to have to write my own file handling. It would be nice if readFile and writeFile would return nil or an error so I could handle it myself.

A contrived example of the case:

binser = require("binser")
t = { data = "someData"}
data = binser.readFile("myfile")

if data then
  data[#data + 1] = t
end

binser.writeFile("myFile", table.unpack(data))

In my opinion, what I would like to see from readFile/writeFile might be something like this:

local function readFile(path)
  local file, err = io.open(path, "rb")
  if not file then return nil end
  -- assert(file, err)
  local str = file:read("*all")
  file:close()
  return deserialize(str)
end

deserialing not have instruction

Please add instruction to this function, i waste 3 days to find a way to DECODE serialized files.

Writing is simple
local mydata = binser.serialize(uammokey,uicekey,ubombkey,udropkey,uleftkey,urightkey,uupkey,udownkey)
sourcewrite(mydata, "keyboardsettings");

But READING is real hardcore.
keyboardloadingdata=sourceread ("keyboardsettings");
if (keyboardloadingdata~=0) then
ua= binser.deserialize (keyboardloadingdata)
uammokey =ua[1];--split("!") ;
uicekey = ua[2];
ubombkey = ua[3];
udropkey = ua[4];
uleftkey = ua[5];
urightkey = ua[6];
uupkey = ua[7];
udownkey = ua[8];
uunpackkey = ua[9];
end

i tries reads data wrong methods like this
uammokey, uice, ubomb,uleft...= ua

Priting a serialized table gives unicode symbols

Sorry for the very stupid question. I'm trying to print a simple table like so :

grid = {}
for i = 1, x do
for j = 1, y do
        grid[i] = {}
	grid[i][j] = 1
end
end

print(binser.s(grid))

All this gives as an output is :

�+���'����'����'����'����'����'����'����'����'����'����'����'����'����'����'����'��

What I'm doing wrong?

fairly simple issue

Good day,
This currently fairly simple code will return me an error when I try to to deserialize.

binser = require "includes/binser"
local player = {}
--
function love.load()
    if love.filesystem.exists( 'Save.lua' ) then
        player = binser.d(love.filesystem.load('Save.lua'))
    else
        player[1] = "foo"
        player[2] = "bar"
    end
end
--
function love.quit()
   love.filesystem.write( 'Save.lua', binser.s(player) )
end

actual error:
binsererror
content of save file:
binsererror2

Thank you.

Error on malformed input when looking ahead

binser sometimes optimistically works with bytes at indexes that may be past end of input. E.g.

local binser = require "binser"
binser.deserialize("\128")
binser.lua:347: attempt to perform arithmetic on a nil value
stack traceback:
	binser.lua:347: in upvalue 'deserialize_value'
	binser.lua:455: in function 'binser.deserialize'
	(...tail calls...)
	[C]: in ?

(this is on fix-next branch)

This technically fits the "binser throws on malformed input" contract but makes it impossible to distinguish deseriazation failures due to bad input from regular Lua errors (bugs happen, don't want to silence/misinterpret them). It would be even nicer if there was a way to get nil + error message on malformed input.

resource example

Hi,

I am trying to use binser to serialize data from lua. Based on the readme, it looks perfect, as I need to be able to include binary data into the files. That said, I have no clue how to do that, based on the readme I'd guess that I'd use registerResource to achieve that but I can't really make sense of it based on the readme. Can someone provide me with a full example of how to do that i.e. with an image?

Thank you!

Serialized types get mixed

I was using most likely b403348
The serialized and deserialized values are a bit different, though this is nothing that important.

local binser = require("binser")
local tbl = {1/0, 0/0, -(1/0), -(0/0), math.huge, -math.huge}
local data = binser.deserialize(binser.serialize(tbl))
print(table.unpack(data))
print(table.unpack(tbl))

-- output:
inf     -nan(ind)       -inf    -nan(ind)       inf     -inf
inf     -nan(ind)       -inf    nan             inf     -inf

A similar issue was fixed here: gvx/Smallfolk#3

ps. I dont suppose its a one line change to make \0not appear in the serialized sequence?
Assuming it does appear in it.
Otherwise Ill encode the output string not to be able to represent the null character.

Lua 5.3 supported ?

Hi,

i tried today the binser but i get this error message: (number has no integer representation)
So my table contains a few floating point values since can distinguish between them since lua 5.3.
Is Lua 5.3 fully supported with floating point values ?

Add a version field

It would be nice to have a field (e.g. binser.version = "0.0-6") so that programs depending on binser could show its version in their --version message. This makes it easy to identify user environment when investigating issues.

(Side note: IIRC rockspec revisions are supposed to be for metadata/dependency/build instruction updates and should not point to different sources, perhaps something semver-like could be used like 0.0.6?)

binser.unregister does not work.

minimal code example:

local mt = {}

binser.register(mt, "foo")

binser.unregister("foo")

binser.register(mt, "bar") -- Error: Resource already registered

Update deserialize/N doc

Hi,

Just to let you know that the doc for deserialize and deserializedN in the README are missing the index argument.

In README
local function deserialize(str)

in the source
local function deserialize(str, index)

For instance the following code will not work as expected if the user doesnt know about the second index argument.

binser.deserialize(love.filesystem.read('myfile.ser'))

Having some trouble deserializing a third-party class

In the current LOVE game I am building, I use the rotLove library, which uses 30log classes. I've managed to serialize my own 30log classes, but I'm getting an error when I try to load now - an error I was not getting until I added rotLove's game scheduler and engine to my game state to be serialized.

S to save, Shift-Q to quit
truesteel.love.zip

Is binser safe?

The README says that binser "does not [...] use the Lua parser to read expressions" and that "[t]his makes it safe."

But the deserializer uses loadstring() for functions.

Doesn't this mean that binser is in fact not safe? Perhaps deserialize() could take an argument to enable the non-safe behavior of deserializing functions?

Table duplicated

I'm trying to serialize a rather complex and large table. Good news is, binser is really fast to do so.
Bad news is there's some self-referencing that it doesn't seem to recognize.

I have a hierarchy like so:

level -> grid[x][y] -> units[x] -> actions[i] -> data -> level

Where level should be equal to data.level. I get two copies of the same level.

Tables not serializing with nested registered userdata.

I usually hate to bother people with my specific game's code, but I've had a bit of issue getting binser to operate correctly here. So, in this example love file I am able to use binser to register a quad userdata, save the table that contains instances of a class that contains a quad (f5), manipulate those objects in some way (f6), and then load them back (f7).

On lines 90 of main.lua we wrap the quad's userdata and register it, on line 312 we register the table that contains the instances of the 'static' objects, and on line 1300 we do the actual save/manip/restore.

I had to manipulate binser so that it did not attempt to serialize a key/value pair of nil/nil by adding:

if k~=nil then
    ret[k] = v
else
    print(k, v)
end

to line 309 of binser.lua

I was under the impression that I should be able to serialize a plain table no problems so I think I might be off on the wrong foot trying to register or serialize the tables the way I am.

Deserialize return value

This is just some feedback, but from user stand point the vararg return is very inconvenient.

TL;DR
To process the return values from deserialize a new function needs to be created OR must assume there are no nils and they must be tabled like {binser.deserialize(str)} to for loop them. Also this is complicated more by the fact that deserialize can error out and pcall is needed.

To process the results you must loop them through unless you expect an exact amount of results like 3 and store them in variables.
Looping the results must be done either by select or making a table of the results and using select("#", ...) to get the amount of results. Doing so requires another function to be made so you can actually use the vararg variable at all ....

In addition to this, the serialize and deserialize functions can error out.
Lets assume you want to load game data and want to handle any errors from deserializing.
To do that you would need to wrap the deserialize in a pcall, which returns true or false and then error message or the function return value.

Below I made the easiest way to do this, which requires the result of pcall to be directly passed to another function that processes the results. Otherwise I could table all the return values and test if index 1 has true or false and then continue or possibly remove that value.

local process_varargs(process, success, ...)
    assert(process, success) -- assert or some other error handler
    for i = 1, select("#", ...) do
        process(select(i, ...))
    end
    return true
end

process_varargs(process, pcall(binser.deserialize, str))

If the table and len was returned directly from deserialize without unpacking, it would be very easy to unpack the table if needed and otherwise it would be easy to handle the table and it would also be more efficient. For example

local success, results, len = pcall(binser.deserialize, str)
assert(success, results) -- assert or some other error handler
for i = 1, len do
    process(results[i])
end

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.