-- serialize.lua-- Lua-conformant library file that has no minetest dependencies-- Contains the serialization and deserialization routines--[[Version history:1 - initial2 - also escaping CR character as &rStructure 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 matchingEexample:LUA_SER v=2 {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'':' -> '&:'All other characters are unchanged as they bear no special meaning.]]local write_table, literal_to_string, escape_chars, table_is_empty
functiontable_is_empty(t)for _,_ inpairs(t)doreturn falseendreturn trueendfunctionwrite_table(t, file, config)local ks, vs, writeit, istable
for key, value inpairs(t)do
ks =value_to_string(key,false)
writeit =true
istable =type(value)=="table"if istable then
vs ="T"if config and config.skip_empty_tables then
writeit =nottable_is_empty(value)endelse
vs =value_to_string(value,true)endif writeit then
file:write(ks..":"..vs.."\n")if istable thenwrite_table(value, file, config)
file:write("E\n")endendendendfunctionvalue_to_string(t)iftype(t)=="table"then
file:close()error("Can not serialize a table in the key position!")elseiftype(t)=="boolean"thenif t thenreturn"B1"elsereturn"B0"endelseiftype(t)=="number"thenreturn"N"..t
elseiftype(t)=="string"thenreturn"S"..escape_chars(t)else--error("Can not serialize '"..type(t).."' type!")return"S<function>"endreturn str
endfunctionescape_chars(str)local rstr =string.gsub(str,"&","&&")
rstr =string.gsub(rstr,":","&:")
rstr =string.gsub(rstr,"\r","&r")
rstr =string.gsub(rstr,"\n","&n")return rstr
end------local read_table, string_to_value, unescape_chars
functionread_table(t, file)local line, ks, vs, kv, vv, vt
while true do
line = file:read("*l")if not line then
file:close()error("Unexpected EOF or read error!")end-- possibly windows fix: strip trailing \r's from line
line =string.gsub(line,"\r$","")if line=="E"then-- done with this tablereturnend
ks, vs =string.match(line,"^(.*[^&]):(.+)$")if not ks or not vs then
file:close()error("Unable to parse line: '"..line.."'!")end
kv =string_to_value(ks)
vv, vt =string_to_value(vs,true)if vt thenread_table(vv, file)end-- put read value in table
t[kv] = vv
endend-- returns: value, is_tablefunctionstring_to_value(str, table_allow)local first =string.sub(str,1,1)local rest =string.sub(str,2)if first=="T"thenif table_allow thenreturn{},trueelse
file:close()error("Table not allowed in key component!")endelseif first=="N"thenlocal num =tonumber(rest)if num thenreturn num
else
file:close()error("Unable to parse number: '"..rest.."'!")endelseif first=="B"thenif rest=="0"thenreturn falseelseif rest=="1"thenreturn trueelse
file:close()error("Unable to parse boolean: '"..rest.."'!")endelseif first=="S"thenreturnunescape_chars(rest)else
file:close()error("Unknown literal type '"..first.."' for literal '"..str.."'!")endendfunctionunescape_chars(str)--TODOlocal rstr =string.gsub(str,"&:",":")
rstr =string.gsub(rstr,"&n","\n")
rstr =string.gsub(rstr,"&r","\r")
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 filelocal functionwrite_to_fd(root_table, file, config)
file:write("LUA_SER v=2\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. Closes the file.-- config: see abovelocal functionread_from_fd(file)local first_line = file:read("*line")-- possibly windows fix: strip trailing \r's from line
first_line =string.gsub(first_line,"\r$","")if notstring.match(first_line,"LUA_SER v=[12]")then
file:close()error("Expected header, got '"..first_line.."' instead!")endlocal t = {}read_table(t, file)local last_line = file:read("*line")-- possibly windows fix: strip trailing \r's from line
last_line =string.gsub(last_line,"\r$","")
file:close()if last_line ~="END_SER"thenerror("Missing END_SER, got '"..last_line.."' instead!")endreturn t
end-- Opens the passed filename and serializes root_table into it-- config: see abovefunctionwrite_to_file(root_table, filename, config)-- try opening the filelocal file, err = io.open(filename,"wb")if not file thenerror("Failed opening file '"..filename.."' for write:\n"..err)endwrite_to_fd(root_table, file, config)return trueend-- Opens the passed filename, and returns its deserialized contentsfunctionread_from_file(filename)-- try opening the filelocal file, err = io.open(filename,"rb")if not file thenerror("Failed opening file '"..filename.."' for read:\n"..err)endreturnread_from_fd(file)end--[[ simple unit testlocal 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,}