diff options
Diffstat (limited to 'serialize.lua')
-rw-r--r-- | serialize.lua | 260 |
1 files changed, 260 insertions, 0 deletions
diff --git a/serialize.lua b/serialize.lua new file mode 100644 index 0000000..8ffd917 --- /dev/null +++ b/serialize.lua @@ -0,0 +1,260 @@ +-- serialize.lua +-- Lua-conformant library file that has no minetest dependencies +-- Contains the serialization and deserialization routines + +--[[ + +Structure of entry: +[keytype][key]:[valuetype][val] +Types: + B - bool + -> 0=false, 1=true + S - string + -> see below + N - number + -> thing compatible with tonumber() +Table: +[keytype][key]:T +... content is nested in table until the matching +E + +example: +LUA_SER v=1 { +Skey:Svalue key = "value", +N1:Seins [1] = "eins", +B1:T [true] = { +Sa:Sb a = "b", +Sc:B0 c = false, +E } +E } + +String representations: +In strings the following characters are escaped by & +'&' -> '&&' +(line break) -> '&n' +(CR) -> '&r' --> required? +':' -> '&:' +All other characters are unchanged as they bear no special meaning. +]] + +local write_table, literal_to_string, escape_chars, table_is_empty + +function table_is_empty(t) + for _,_ in pairs(t) do + return false + end + return true +end + +function write_table(t, file, config) + local ks, vs, writeit, istable + for key, value in pairs(t) do + ks = value_to_string(key, false) + writeit = true + istable = type(value)=="table" + + if istable then + vs = "T" + if config.skip_empty_tables then + writeit = not table_is_empty(value) + end + else + vs = value_to_string(value, true) + end + + if writeit then + file:write(ks..":"..vs.."\n") + + if istable then + write_table(value, file, config) + file:write("E\n") + end + end + end +end + +function value_to_string(t) + if type(t)=="table" then + error("Can not serialize a table in the key position!") + elseif type(t)=="boolean" then + if t then + return "B1" + else + return "B0" + end + elseif type(t)=="number" then + return "N"..t + elseif type(t)=="string" then + return "S"..escape_chars(t) + else + error("Can not serialize '"..type(t).."' type!") + end + return str +end + +function escape_chars(str) + local rstr = string.gsub(str, "&", "&&") + rstr = string.gsub(rstr, ":", "&:") + rstr = string.gsub(rstr, "\n", "&n") + return rstr +end + +------ + +local read_table, string_to_value, unescape_chars + +function read_table(t, file) + local line, ks, vs, kv, vv, vt + while true do + line = file:read("*l") + if not line then + error("Unexpected EOF or read error!") + end + + if line=="E" then + -- done with this table + return + end + ks, vs = string.match(line, "^(.+[^&]):(.+)$") + if not ks or not vs then + error("Unable to parse line: '"..line.."'!") + end + kv = string_to_value(ks) + vv, vt = string_to_value(vs, true) + if vt then + read_table(vv, file) + end + -- put read value in table + t[kv] = vv + end +end + +-- returns: value, is_table +function string_to_value(str, table_allow) + local first = string.sub(str, 1,1) + local rest = string.sub(str, 2) + if first=="T" then + if table_allow then + return {}, true + else + error("Table not allowed in key component!") + end + elseif first=="N" then + local num = tonumber(rest) + if num then + return num + else + error("Unable to parse number: '"..rest.."'!") + end + elseif first=="B" then + if rest=="0" then + return false + elseif rest=="1" then + return true + else + error("Unable to parse boolean: '"..rest.."'!") + end + elseif first=="S" then + return unescape_chars(rest) + else + error("Unknown literal type '"..first.."' for literal '"..str.."'!") + end +end + +function unescape_chars(str) --TODO + local rstr = string.gsub(str, "&:", ":") + rstr = string.gsub(rstr, "&n", "\n") + rstr = string.gsub(rstr, "&&", "&") + return rstr +end + +------ + +--[[ +config = { + skip_empty_tables = false -- if true, does not store empty tables + -- On next read, keys that mapped to empty tables resolve to nil +} +]] + +-- Writes the passed table into the passed file descriptor, and closes the file afterwards +local function write_to_fd(root_table, file, config) + file:write("LUA_SER v=1\n") + write_table(root_table, file, config) + file:write("E\nEND_SER\n") + file:close() +end + +-- Reads the file contents from the passed file descriptor and returns the table on success +-- Throws errors when something is wrong. +-- config: see above +local function read_from_fd(file) + local first_line = file:read("*l") + if first_line ~= "LUA_SER v=1" then + error("Expected header, got '"..first_line.."' instead!") + end + local t = {} + read_table(t, file) + local last_line = file:read("*l") + file:close() + if last_line ~= "END_SER" then + error("Missing END_SER, got '"..last_line.."' instead!") + end + return t +end + +-- Opens the passed filename and serializes root_table into it +-- config: see above +function write_to_file(root_table, filename, config) + -- try opening the file + local file, err = io.open(filename, "w") + if not file then + error("Failed opening file '"..filename.."' for write: "..err) + end + + write_to_fd(root_table, file, config) + return true +end + +-- Opens the passed filename, and returns its deserialized contents +function read_from_file(filename) + -- try opening the file + local file, err = io.open(filename, "r") + if not file then + error("Failed opening file '"..filename.."' for read: "..err) + end + + return read_from_fd(file) +end + +--[[ simple unit test +local testtable = { + key = "value", + [1] = "eins", + [true] = { + a = "b", + c = false, + }, + ["es:cape1"] = "foo:bar", + ["es&ca\npe2"] = "baz&bam\nbim", + ["es&&ca&\npe3"] = "baz&&bam&\nbim", + ["es&:cape4"] = "foo\n:bar" +} +local config = {} +--write_to_file(testtable, "test_out", config) +local t = read_from_file("test_out") +write_to_file(t, "test_out_2", config) +local t2 = read_from_file("test_out_2") +write_to_file(t2, "test_out_3", config) + +-- test_out_2 and test_out_3 should be equal + +--]] + + +return { + read_from_fd = read_from_fd, + write_to_fd = write_to_fd, + read_from_file = read_from_file, + write_to_file = write_to_file, +} |