From 9774a017a3baadd3caf3b442f4a22a0bf151d4c9 Mon Sep 17 00:00:00 2001 From: OpportunityLiu Date: Wed, 24 Jul 2019 18:36:49 +0800 Subject: Improve string.serialize; add unit test --- tests/modules/string/serialize/test.lua | 36 +++++++++++++++++++ tests/test_utils/check.lua | 3 ++ xmake/core/base/deprecated.lua | 25 +++++++------ xmake/core/base/dump.lua | 23 ++++++------ xmake/core/base/math.lua | 58 ++++++++++++++++++++++++++++++ xmake/core/base/string.lua | 63 ++++++++++++++++++++------------- xmake/core/sandbox/modules/math.lua | 2 +- 7 files changed, 160 insertions(+), 50 deletions(-) create mode 100644 tests/modules/string/serialize/test.lua create mode 100644 xmake/core/base/math.lua diff --git a/tests/modules/string/serialize/test.lua b/tests/modules/string/serialize/test.lua new file mode 100644 index 000000000..8d13f6fee --- /dev/null +++ b/tests/modules/string/serialize/test.lua @@ -0,0 +1,36 @@ + +function roundtrip(v) + return string.serialize(v):deserialize() +end + +function test_number(t) + t:are_equal(roundtrip(12), 12) + t:are_equal(roundtrip(0), 0) + t:are_equal(roundtrip(-1), -1) + t:are_equal(roundtrip(7.25), 7.25) + t:are_equal(roundtrip(math.huge), math.huge) + t:are_equal(roundtrip(-math.huge), -math.huge) + t:are_equal(roundtrip(math.nan), math.nan) +end + +function test_boolean(t) + t:are_equal(roundtrip(true), true) + t:are_equal(roundtrip(false), false) +end + +function test_nil(t) + t:are_equal(roundtrip(nil), nil) +end + +function test_table(t) + t:are_equal(roundtrip({}), {}) + t:are_equal(roundtrip({1, 2, 3}), {1, 2, 3}) + t:are_equal(roundtrip({1, "", 3}), {1, "", 3}) + --t:are_equal(roundtrip({{1, 2, 3, nil, 4}}), {{1, 2, 3, nil, 4}}) +end + +function test_function(t) + t:are_equal(roundtrip(function() return {} end)(), {}) + t:are_equal(roundtrip(function() return {1, 2, 3} end)(), {1, 2, 3}) + t:are_equal(roundtrip(function() return {{1, 2, 3, nil, 4}} end)(), {{1, 2, 3, nil, 4}}) +end diff --git a/tests/test_utils/check.lua b/tests/test_utils/check.lua index bc9ddb6b6..86fa5d6b5 100644 --- a/tests/test_utils/check.lua +++ b/tests/test_utils/check.lua @@ -13,6 +13,9 @@ function _get_rep(value) end function same(actual, expacted) + if actual ~= actual and expacted ~= expacted then + return true, _get_rep(actual), _get_rep(expacted) + end return actual == expacted, _get_rep(actual), _get_rep(expacted) end diff --git a/xmake/core/base/deprecated.lua b/xmake/core/base/deprecated.lua index b37851f20..467bc2964 100644 --- a/xmake/core/base/deprecated.lua +++ b/xmake/core/base/deprecated.lua @@ -21,12 +21,6 @@ -- define module local deprecated = deprecated or {} --- load modules -local utils = require("base/utils") -local table = require("base/table") -local string = require("base/string") -local option = require("base/option") - -- add deprecated entry function deprecated.add(newformat, oldformat, ...) @@ -44,6 +38,10 @@ end -- dump all deprecated entries function deprecated.dump() + -- lazy load modules to avoid loop + local utils = require("base/utils") + local option = require("base/option") + -- dump one or more .. local index = 0 deprecated._ENTRIES = deprecated._ENTRIES or {} @@ -53,17 +51,18 @@ function deprecated.dump() if index == 0 then print("") end - if new then - utils.cprint("${bright color.warning}deprecated: ${clear}please uses %s instead of %s", new, old) - else - utils.cprint("${bright color.warning}deprecated: ${clear}please remove %s", old) - end -- show more? - if not option.get("verbose") then - utils.cprint("${bright color.warning}deprecated: ${clear}add -v for getting more ..") + if not option.get("verbose") and index > 0 then + utils.cprint("${bright color.warning}deprecated:${clear} add -v for getting more ..") break end + + if new then + utils.cprint("${bright color.warning}deprecated:${clear} please uses %s instead of %s", new, old) + else + utils.cprint("${bright color.warning}deprecated:${clear} please remove %s", old) + end index = index + 1 end end diff --git a/xmake/core/base/dump.lua b/xmake/core/base/dump.lua index 199dc1e19..ce3b87b91 100644 --- a/xmake/core/base/dump.lua +++ b/xmake/core/base/dump.lua @@ -72,16 +72,19 @@ end -- print function function dump._print_function(func, as_key) + io.write(dump._translate("${reset}${color.dump.function}")) if as_key then - return dump._print_default(func) - end - local funcinfo = debug.getinfo(func) - local srcinfo = funcinfo.short_src - if funcinfo.linedefined >= 0 then - srcinfo = srcinfo .. ":" .. funcinfo.linedefined + io.write(dump._format("text.dump.default_format", "%s", func)) + else + local funcinfo = debug.getinfo(func) + local srcinfo = funcinfo.short_src + if funcinfo.linedefined >= 0 then + srcinfo = srcinfo .. ":" .. funcinfo.linedefined + end + local funcname = funcinfo.name and (funcinfo.name .. " ") or "" + io.write(dump._translate("function ${bright}"), funcname, dump._translate("${reset}${dim}"), srcinfo) end - local funcname = funcinfo.name and (funcinfo.name .. " ") or "" - io.write(dump._translate("${reset}${color.dump.function}function ${bright}"), funcname, dump._translate("${reset}${dim}"), srcinfo) + io.write(dump._translate("${reset}")) end -- print value with default format @@ -101,9 +104,7 @@ end -- print scalar value function dump._print_scalar(value, as_key) - if type(value) == "nil" then - dump._print_keyword("nil") - elseif type(value) == "boolean" then + if type(value) == "nil" or type(value) == "boolean" then dump._print_keyword(value) elseif type(value) == "number" then dump._print_number(value) diff --git a/xmake/core/base/math.lua b/xmake/core/base/math.lua new file mode 100644 index 000000000..a2dad7a6e --- /dev/null +++ b/xmake/core/base/math.lua @@ -0,0 +1,58 @@ +--!A cross-platform build utility based on Lua +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2019, TBOOX Open Source Group. +-- +-- @author OpportunityLiu +-- @file math.lua +-- + +-- define module +local math = math or {} + +-- init constants +math.nan = math.log(-1) +math.e = math.exp(1) + +-- check a number is inf or -inf +-- +-- @returns 1 for inf, -1 for -inf, otherwise false +function math:isinf() + + -- check + assert(type(self) == "number", "number expacted") + + if self == math.huge then + return 1 + elseif self == -math.huge then + return -1 + else + return false + end +end + +-- check a number is nan +-- +-- @returns true for nan, otherwise false +function math:isnan() + + -- check + assert(type(self) == "number", "number expacted") + + return self ~= self +end + + +-- return module +return math diff --git a/xmake/core/base/string.lua b/xmake/core/base/string.lua index b59706a6d..d3a584f3e 100644 --- a/xmake/core/base/string.lua +++ b/xmake/core/base/string.lua @@ -21,6 +21,9 @@ -- define module: string local string = string or {} +-- load modules +local deprecated = require("base/deprecated") + -- save original interfaces string._dump = string._dump or string.dump string._trim = string._trim or string.trim @@ -29,12 +32,30 @@ string._trim = string._trim or string.trim function string._makestr(object, deflate, serialize, level) if type(object) == "string" then return serialize and string.format("%q", object) or object - elseif type(object) == "boolean" or type(object) == "number" then + elseif type(object) == "boolean" or type(object) == "nil" then + return tostring(object) + elseif type(object) == "number" then + if serialize then + if math.isnan(object) then + return "math.nan" + end + local inf = math.isinf(object) + if inf == 1 then + return "math.huge" + elseif inf == -1 then + return "-math.huge" + end + end return tostring(object) elseif not serialize and type(object) == "table" and (getmetatable(object) or {}).__tostring then return tostring(object) elseif type(object) == "table" then + local indent = "" + if not deflate then + indent = string.rep(" ", level) + end + -- make head local s = "" if deflate then @@ -43,41 +64,36 @@ function string._makestr(object, deflate, serialize, level) if level > 0 then s = s .. "\n" end - for l = 1, level do - s = s .. " " - end - s = s .. "{\n" + s = s .. indent .. "{\n" end -- make body local i = 0 - for k, v in pairs(object) do + for k, v in pairs(object) do if deflate then s = s .. (i ~= 0 and "," or "") else - for l = 1, level do - s = s .. " " - end + s = s .. indent if i == 0 then s = s .. " " else s = s .. ", " end end - + -- make key = value if type(k) == "string" then - if serialize and not k:match("^%a[%w_]+$") then + if serialize and not k:match("^[%a_][%w_]+$") then k = string.format("[%q]", k) end if deflate then - s = s .. k .. "=" + s = s .. k .. "=" else - s = s .. k .. " = " + s = s .. k .. " = " end end - local substr, errors = string._makestr(v, deflate, serialize, level + 1) + local substr, errors = string._makestr(v, deflate, serialize, level + 1) if substr == nil then return nil, errors end @@ -87,17 +103,12 @@ function string._makestr(object, deflate, serialize, level) s = s .. "\n" end i = i + 1 - end + end -- make tail - if not deflate then - for l = 1, level do - s = s .. " " - end - end - s = s .. "}" + s = s .. indent .. "}" return s - elseif serialize and type(object) == "function" then + elseif serialize and type(object) == "function" then return string.format("%q", string._dump(object)) elseif serialize then return nil, "cannot serialize object: " .. type(object) @@ -113,7 +124,7 @@ function string._loadstr(object) -- only load luajit function data: e.g. "\27LJ\2\0\6=stdin" if type(object) == "string" and object:startswith("\27LJ") then return loadstring(object) - elseif type(object) == "table" then + elseif type(object) == "table" then for k, v in pairs(object) do local value, errors = string._loadstr(v) if value ~= nil then @@ -286,6 +297,7 @@ function string.ipattern(pattern, brackets) return table.concat(tmp) end +-- @deprecated -- dump to string from the given object (more readable) -- -- @param deflate deflate empty characters @@ -293,6 +305,7 @@ end -- @return string, errors -- function string.dump(object, deflate) + deprecated.add("utils.dump() or string.serialize()", "string.dump()") return string._makestr(object, deflate, false, 0) end @@ -318,10 +331,10 @@ function string:deserialize() local result = nil local script, errors = loadstring("return " .. self) if script then - + -- load object local ok, object = pcall(script) - if ok and object then + if ok then result = object elseif object then -- error diff --git a/xmake/core/sandbox/modules/math.lua b/xmake/core/sandbox/modules/math.lua index ebaf989b0..a52b13310 100644 --- a/xmake/core/sandbox/modules/math.lua +++ b/xmake/core/sandbox/modules/math.lua @@ -19,5 +19,5 @@ -- -- load module -return math +return require("base/math") -- cgit v1.3.1 From 95169f0ec740f0466dfab97424e3c72460764f17 Mon Sep 17 00:00:00 2001 From: OpportunityLiu Date: Thu, 25 Jul 2019 15:25:38 +0800 Subject: add some tests --- tests/modules/string/serialize/test.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/modules/string/serialize/test.lua b/tests/modules/string/serialize/test.lua index 8d13f6fee..1e1484f98 100644 --- a/tests/modules/string/serialize/test.lua +++ b/tests/modules/string/serialize/test.lua @@ -26,7 +26,8 @@ function test_table(t) t:are_equal(roundtrip({}), {}) t:are_equal(roundtrip({1, 2, 3}), {1, 2, 3}) t:are_equal(roundtrip({1, "", 3}), {1, "", 3}) - --t:are_equal(roundtrip({{1, 2, 3, nil, 4}}), {{1, 2, 3, nil, 4}}) + t:are_equal(roundtrip({{1, 2, 3, nil, 4}}), {{1, 2, 3, nil, 4}}) + t:are_equal(roundtrip({{a=1, b=2, c=3, nil, 4}}), {{a=1, b=2, c=3, nil, 4}}) end function test_function(t) -- cgit v1.3.1 From 980fe707e37687ceb5a2653298001845377136ee Mon Sep 17 00:00:00 2001 From: OpportunityLiu Date: Thu, 25 Jul 2019 17:43:55 +0800 Subject: refactor serialize functions --- xmake/core/base/math.lua | 11 ++ xmake/core/base/serialize.lua | 272 ++++++++++++++++++++++++++++++++++++++++++ xmake/core/base/string.lua | 146 ++--------------------- 3 files changed, 290 insertions(+), 139 deletions(-) create mode 100644 xmake/core/base/serialize.lua diff --git a/xmake/core/base/math.lua b/xmake/core/base/math.lua index a2dad7a6e..d49e8a8b9 100644 --- a/xmake/core/base/math.lua +++ b/xmake/core/base/math.lua @@ -25,6 +25,17 @@ local math = math or {} math.nan = math.log(-1) math.e = math.exp(1) +-- check a number is int +-- +-- @returns true for int, otherwise false +function math:isint() + + -- check + assert(type(self) == "number", "number expacted") + + return self == math.floor(self) and self ~= math.huge and self ~= -math.huge +end + -- check a number is inf or -inf -- -- @returns 1 for inf, -1 for -inf, otherwise false diff --git a/xmake/core/base/serialize.lua b/xmake/core/base/serialize.lua new file mode 100644 index 000000000..0af3a27a0 --- /dev/null +++ b/xmake/core/base/serialize.lua @@ -0,0 +1,272 @@ + +--!A cross-platform build utility based on Lua +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- Copyright (C) 2015 - 2019, TBOOX Open Source Group. +-- +-- @author OpportunityLiu +-- @file serialize.lua +-- + +-- define module: serialize +local serialize = serialize or {} + +-- load modules +local math = require("base/math") + +-- save original interfaces +serialize._dump = serialize._dump or string._dump or string.dump + +serialize._FUNCTAG = "\27FUNCTION" + +function serialize._makenumber(num, opt, level) + if math.isnan(num) then + return "math.nan" + end + local inf = math.isinf(num) + if inf == 1 then + return "math.huge" + elseif inf == -1 then + return "-math.huge" + end + return tostring(num) +end + +function serialize._makestring(str, opt, level) + return string.format("%q", str) +end + +function serialize._makekeyword(val, opt, level) + return tostring(val) +end + +function serialize._maketable(object, opt, level) + + local childlevel = level + 1 + + -- serialize child items + local serialized = {} + local numidxcount = 0 + local isarr = true + local maxn = 0 + for k, v in pairs(object) do + if type(k) == "number" then + numidxcount = numidxcount + 1 + if k < 1 or not math.isint(k) then + isarr = false + elseif k > maxn then + maxn = k + end + elseif type(k) == "string" then + isarr = false + else + return nil, string.format("cannot serialize table with key of %s: %s", type(k), tostring(k)) + end + serialized[k] = serialize._make(v, opt, childlevel) + end + + -- too sparse + if numidxcount * 2 < maxn then + isarr = false + end + + -- make indent + local indent = "" + if opt.indent then + indent = string.rep(" ", level) + end + + -- make head + local headstr + if opt.indent then + headstr = (level > 0 and "\n" or "") .. indent .. "{\n" + else + headstr = "{" + end + -- make tail + local tailstr + if opt.indent then + tailstr = "\n" .. indent .. "}" + else + tailstr = "}" + end + + -- make body + local s = {} + if opt.indent then + indent = indent .. " " + end + + if isarr then + local nilval + if maxn ~= numidxcount then + nilval = indent .. "nil" + end + for i = 1, maxn do + local val = serialized[i] + if val == nil then + s[i] = nilval + else + s[i] = indent .. val + end + end + else + local con = opt.indent and " = " or "=" + for k, v in pairs(serialized) do + if type(k) == "string" and not k:match("^[%a_][%w_]*$") then + k = string.format("[%q]", k) + elseif type(k) == "number" then + k = string.format("[%s]", serialize._makenumber(k, opt, childlevel)) + end + table.insert(s, indent .. k .. con .. v) + end + end + return headstr .. table.concat(s, opt.indent and ",\n" or ",") .. tailstr +end + +function serialize._makefunction(func, opt, level) + local ok, funccode = pcall(serialize._dump, func, opt.strip) + if not ok then + return nil, funccode + end + local closure = {} + -- local nups = debug.getinfo(func, 'u').nups + -- for i = 1, nups do + -- local k, v = debug.getupvalue(func, i) + -- if _G[k] == v then + -- v = nil + -- else + -- v = "haha" + -- end + -- closure[i] = { k, v } + -- end + -- closure.nups = nups + closure.func = funccode + closure.tag = serialize._FUNCTAG + return serialize._maketable(closure, opt, level + 1) +end + +-- make string with the level +function serialize._make(object, opt, level) + + -- call make* by type + if type(object) == "string" then + return serialize._makestring(object, opt, level) + elseif type(object) == "boolean" or type(object) == "nil" then + return serialize._makekeyword(object, opt, level) + elseif type(object) == "number" then + return serialize._makenumber(object, opt, level) + elseif type(object) == "table" then + return serialize._maketable(object, opt, level) + elseif type(object) == "function" then + return serialize._makefunction(object, opt, level) + else + return nil, string.format("cannot serialize %s: '%s'", type(object), tostring(object)) + end +end + +-- serialize to string from the given object +-- +-- @param opt serialize options +-- +-- @return string, errors +-- +function serialize.save(object, opt) + + -- init options + opt = opt or {} + + -- make string + local result, errors = serialize._make(object, opt, 0) + + -- ok? + if errors ~= nil then + return nil, errors + end + return result +end + +function serialize._loadfunction(closure) + local func, err = loadstring(closure.func, "=(deserialized code)") + if func == nil then + return nil, err + end + -- for i = 1, closure.nups do + -- local upval = closure[i] + -- if upval[2] == nil then + -- upval[2] = _G[upval[1]] + -- end + -- debug.setupvalue(func, i, upval[2]) + -- end + return func +end + +-- load table from string in table +function serialize._load(object) + if type(object) == "table" then + if object.tag == serialize._FUNCTAG then + return serialize._loadfunction(object) + end + for k, v in pairs(object) do + local value, errors = serialize._load(v) + if value ~= nil then + object[k] = value + else + return nil, errors + end + end + end + return object +end + +-- deserialize string to object +-- +-- @param str the serialized string +-- +-- @return object, errors +-- +function serialize.load(str) + + -- load table as script + local result = nil + local script, errors = loadstring("return " .. str) + if script then + + -- load object + local ok, object = pcall(script) + if ok then + result = object + elseif object then + -- error + errors = object + else + -- error + errors = string.format("cannot deserialize string: %s", str) + end + end + + -- load function from string in table + if result then + result, errors = serialize._load(result) + end + + -- ok? + if errors ~= nil then + return nil, errors + end + return result +end + +-- return module: serialize +return serialize diff --git a/xmake/core/base/string.lua b/xmake/core/base/string.lua index d3a584f3e..30c374def 100644 --- a/xmake/core/base/string.lua +++ b/xmake/core/base/string.lua @@ -23,120 +23,12 @@ local string = string or {} -- load modules local deprecated = require("base/deprecated") +local serialize = require("base/serialize") -- save original interfaces string._dump = string._dump or string.dump string._trim = string._trim or string.trim --- make string with the level -function string._makestr(object, deflate, serialize, level) - if type(object) == "string" then - return serialize and string.format("%q", object) or object - elseif type(object) == "boolean" or type(object) == "nil" then - return tostring(object) - elseif type(object) == "number" then - if serialize then - if math.isnan(object) then - return "math.nan" - end - local inf = math.isinf(object) - if inf == 1 then - return "math.huge" - elseif inf == -1 then - return "-math.huge" - end - end - return tostring(object) - elseif not serialize and type(object) == "table" and (getmetatable(object) or {}).__tostring then - return tostring(object) - elseif type(object) == "table" then - - local indent = "" - if not deflate then - indent = string.rep(" ", level) - end - - -- make head - local s = "" - if deflate then - s = s .. "{" - else - if level > 0 then - s = s .. "\n" - end - s = s .. indent .. "{\n" - end - - -- make body - local i = 0 - for k, v in pairs(object) do - - if deflate then - s = s .. (i ~= 0 and "," or "") - else - s = s .. indent - if i == 0 then - s = s .. " " - else - s = s .. ", " - end - end - - -- make key = value - if type(k) == "string" then - if serialize and not k:match("^[%a_][%w_]+$") then - k = string.format("[%q]", k) - end - if deflate then - s = s .. k .. "=" - else - s = s .. k .. " = " - end - end - local substr, errors = string._makestr(v, deflate, serialize, level + 1) - if substr == nil then - return nil, errors - end - s = s .. substr - - if not deflate then - s = s .. "\n" - end - i = i + 1 - end - - -- make tail - s = s .. indent .. "}" - return s - elseif serialize and type(object) == "function" then - return string.format("%q", string._dump(object)) - elseif serialize then - return nil, "cannot serialize object: " .. type(object) - elseif object ~= nil then - return "<" .. tostring(object) .. ">" - else - return "nil" - end -end - --- load table from string in table -function string._loadstr(object) - -- only load luajit function data: e.g. "\27LJ\2\0\6=stdin" - if type(object) == "string" and object:startswith("\27LJ") then - return loadstring(object) - elseif type(object) == "table" then - for k, v in pairs(object) do - local value, errors = string._loadstr(v) - if value ~= nil then - object[k] = value - else - return nil, errors - end - end - end - return object -end - -- find the last substring with the given pattern function string:find_last(pattern, plain) @@ -306,7 +198,7 @@ end -- function string.dump(object, deflate) deprecated.add("utils.dump() or string.serialize()", "string.dump()") - return string._makestr(object, deflate, false, 0) + return string.serialize(object, deflate) end -- serialize to string from the given object @@ -314,9 +206,10 @@ end -- @param deflate deflate empty characters -- -- @return string, errors --- +-- function string.serialize(object, deflate) - return string._makestr(object, deflate, true, 0) + deflate = not not deflate + return serialize.save(object, { strip = deflate, indent = not deflate }) end -- deserialize string to object @@ -324,34 +217,9 @@ end -- @param str the serialized string -- -- @return object, errors --- +-- function string:deserialize() - - -- load table as script - local result = nil - local script, errors = loadstring("return " .. self) - if script then - - -- load object - local ok, object = pcall(script) - if ok then - result = object - elseif object then - -- error - errors = object - else - -- error - errors = string.format("cannot deserialize string: %s", self) - end - end - - -- load function from string in table - if result then - result, errors = string._loadstr(result) - end - - -- ok? - return result, errors + return serialize.load(self) end -- return module: string -- cgit v1.3.1 From cb1283c1f4590aaa562ae9a472a027d27229ba84 Mon Sep 17 00:00:00 2001 From: OpportunityLiu Date: Thu, 25 Jul 2019 18:27:14 +0800 Subject: fix --- xmake/core/base/serialize.lua | 110 +++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 66 deletions(-) diff --git a/xmake/core/base/serialize.lua b/xmake/core/base/serialize.lua index 0af3a27a0..2582daae6 100644 --- a/xmake/core/base/serialize.lua +++ b/xmake/core/base/serialize.lua @@ -28,8 +28,6 @@ local math = require("base/math") -- save original interfaces serialize._dump = serialize._dump or string._dump or string.dump -serialize._FUNCTAG = "\27FUNCTION" - function serialize._makenumber(num, opt, level) if math.isnan(num) then return "math.nan" @@ -71,9 +69,13 @@ function serialize._maketable(object, opt, level) elseif type(k) == "string" then isarr = false else - return nil, string.format("cannot serialize table with key of %s: %s", type(k), tostring(k)) + return nil, string.format("cannot serialize table with key of %s: <%s>", type(k), k) + end + local sval, err = serialize._make(v, opt, childlevel) + if err ~= nil then + return nil, err end - serialized[k] = serialize._make(v, opt, childlevel) + serialized[k] = sval end -- too sparse @@ -127,7 +129,11 @@ function serialize._maketable(object, opt, level) if type(k) == "string" and not k:match("^[%a_][%w_]*$") then k = string.format("[%q]", k) elseif type(k) == "number" then - k = string.format("[%s]", serialize._makenumber(k, opt, childlevel)) + local nval, err = serialize._makenumber(v, opt, childlevel) + if err ~= nil then + return nil, err + end + k = string.format("[%s]", nval) end table.insert(s, indent .. k .. con .. v) end @@ -138,23 +144,21 @@ end function serialize._makefunction(func, opt, level) local ok, funccode = pcall(serialize._dump, func, opt.strip) if not ok then - return nil, funccode + return nil, string.format("%s: <%s>", funccode, func) + end + local chunkname = nil + local sep = "," + if opt.strip then + chunkname = "\"=(deserialized code)\"" + end + if opt.indent then + sep = ", " + end + if chunkname then + return string.format("loadstring(%q%s%s)", funccode, sep, chunkname) + else + return string.format("loadstring(%q)", funccode) end - local closure = {} - -- local nups = debug.getinfo(func, 'u').nups - -- for i = 1, nups do - -- local k, v = debug.getupvalue(func, i) - -- if _G[k] == v then - -- v = nil - -- else - -- v = "haha" - -- end - -- closure[i] = { k, v } - -- end - -- closure.nups = nups - closure.func = funccode - closure.tag = serialize._FUNCTAG - return serialize._maketable(closure, opt, level + 1) end -- make string with the level @@ -172,7 +176,7 @@ function serialize._make(object, opt, level) elseif type(object) == "function" then return serialize._makefunction(object, opt, level) else - return nil, string.format("cannot serialize %s: '%s'", type(object), tostring(object)) + return nil, string.format("cannot serialize %s: <%s>", type(object), object) end end @@ -197,50 +201,12 @@ function serialize.save(object, opt) return result end -function serialize._loadfunction(closure) - local func, err = loadstring(closure.func, "=(deserialized code)") - if func == nil then - return nil, err - end - -- for i = 1, closure.nups do - -- local upval = closure[i] - -- if upval[2] == nil then - -- upval[2] = _G[upval[1]] - -- end - -- debug.setupvalue(func, i, upval[2]) - -- end - return func -end - -- load table from string in table -function serialize._load(object) - if type(object) == "table" then - if object.tag == serialize._FUNCTAG then - return serialize._loadfunction(object) - end - for k, v in pairs(object) do - local value, errors = serialize._load(v) - if value ~= nil then - object[k] = value - else - return nil, errors - end - end - end - return object -end - --- deserialize string to object --- --- @param str the serialized string --- --- @return object, errors --- -function serialize.load(str) +function serialize._load(str) -- load table as script local result = nil - local script, errors = loadstring("return " .. str) + local script, errors = loadstring("return " .. str, str) if script then -- load object @@ -256,10 +222,22 @@ function serialize.load(str) end end - -- load function from string in table - if result then - result, errors = serialize._load(result) - end + return result, errors +end + +-- deserialize string to object +-- +-- @param str the serialized string +-- +-- @return object, errors +-- +function serialize.load(str) + + -- check + assert(str) + + -- load string + local result, errors = serialize._load(str) -- ok? if errors ~= nil then -- cgit v1.3.1 From 5e8049c85f976305206ed2117b71168b1972704c Mon Sep 17 00:00:00 2001 From: OpportunityLiu Date: Thu, 25 Jul 2019 18:31:53 +0800 Subject: add some tests --- tests/modules/string/serialize/test.lua | 3 +++ xmake/core/base/serialize.lua | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/modules/string/serialize/test.lua b/tests/modules/string/serialize/test.lua index 1e1484f98..a85868e7e 100644 --- a/tests/modules/string/serialize/test.lua +++ b/tests/modules/string/serialize/test.lua @@ -27,6 +27,7 @@ function test_table(t) t:are_equal(roundtrip({1, 2, 3}), {1, 2, 3}) t:are_equal(roundtrip({1, "", 3}), {1, "", 3}) t:are_equal(roundtrip({{1, 2, 3, nil, 4}}), {{1, 2, 3, nil, 4}}) + t:are_equal(roundtrip({{1, 2, 3, nil, 4, [100]=5}}), {{1, 2, 3, nil, 4, [100]=5}}) t:are_equal(roundtrip({{a=1, b=2, c=3, nil, 4}}), {{a=1, b=2, c=3, nil, 4}}) end @@ -34,4 +35,6 @@ function test_function(t) t:are_equal(roundtrip(function() return {} end)(), {}) t:are_equal(roundtrip(function() return {1, 2, 3} end)(), {1, 2, 3}) t:are_equal(roundtrip(function() return {{1, 2, 3, nil, 4}} end)(), {{1, 2, 3, nil, 4}}) + t:are_equal(roundtrip({function() return {{1, 2, 3, nil, 4}} end})[1](), {{1, 2, 3, nil, 4}}) + t:are_equal(roundtrip({{function() return {{1, 2, 3, nil, 4}} end}})[1][1](), {{1, 2, 3, nil, 4}}) end diff --git a/xmake/core/base/serialize.lua b/xmake/core/base/serialize.lua index 2582daae6..84c74cd34 100644 --- a/xmake/core/base/serialize.lua +++ b/xmake/core/base/serialize.lua @@ -129,7 +129,7 @@ function serialize._maketable(object, opt, level) if type(k) == "string" and not k:match("^[%a_][%w_]*$") then k = string.format("[%q]", k) elseif type(k) == "number" then - local nval, err = serialize._makenumber(v, opt, childlevel) + local nval, err = serialize._makenumber(k, opt, childlevel) if err ~= nil then return nil, err end -- cgit v1.3.1 From 68f96b225c4da758a87a90f5723025be7b2fafc1 Mon Sep 17 00:00:00 2001 From: OpportunityLiu Date: Thu, 25 Jul 2019 19:15:32 +0800 Subject: add binary mode --- tests/modules/string/serialize/test.lua | 7 ++++-- xmake/core/base/serialize.lua | 43 ++++++++++++++++++++++++++++++--- xmake/core/base/string.lua | 8 +++--- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/tests/modules/string/serialize/test.lua b/tests/modules/string/serialize/test.lua index a85868e7e..1ab3852ba 100644 --- a/tests/modules/string/serialize/test.lua +++ b/tests/modules/string/serialize/test.lua @@ -1,6 +1,9 @@ -function roundtrip(v) - return string.serialize(v):deserialize() +function roundtrip(round0) + local round1 = string.serialize(round0, false):deserialize() + local round2 = string.serialize(round1, true):deserialize() + local round3 = string.serialize(round2, {binary=true}):deserialize() + return round3 end function test_number(t) diff --git a/xmake/core/base/serialize.lua b/xmake/core/base/serialize.lua index 84c74cd34..8b5b233b1 100644 --- a/xmake/core/base/serialize.lua +++ b/xmake/core/base/serialize.lua @@ -138,6 +138,10 @@ function serialize._maketable(object, opt, level) table.insert(s, indent .. k .. con .. v) end end + + if #s == 0 then + return opt.indent and "{ }" or "{}" + end return headstr .. table.concat(s, opt.indent and ",\n" or ",") .. tailstr end @@ -189,7 +193,11 @@ end function serialize.save(object, opt) -- init options - opt = opt or {} + if opt == true then + opt = { strip = true, binary = false, indent = false } + elseif opt == false or opt == nil then + opt = { strip = false, binary = false, indent = true } + end -- make string local result, errors = serialize._make(object, opt, 0) @@ -198,7 +206,19 @@ function serialize.save(object, opt) if errors ~= nil then return nil, errors end - return result + + if not opt.binary then + return result + end + + -- binary mode + local dump, lerr = serialize._dump(loadstring("return " .. result), true) + if lerr ~= nil then + return nil, lerr + end + + -- return shorter representation + return (#dump < #result) and dump or result end -- load table from string in table @@ -206,7 +226,14 @@ function serialize._load(str) -- load table as script local result = nil - local script, errors = loadstring("return " .. str, str) + + local binary = str:startswith("\27LJ") + + if not binary then + str = "return " .. str + end + + local script, errors = loadstring(str) if script then -- load object @@ -217,8 +244,16 @@ function serialize._load(str) -- error errors = object else + local data + if binary then + data = "" + elseif #str > 20 then + data = str:sub(8, 17) .. "..." + else + data = str:sub(8) + end -- error - errors = string.format("cannot deserialize string: %s", str) + errors = string.format("cannot deserialize string: %s", data) end end diff --git a/xmake/core/base/string.lua b/xmake/core/base/string.lua index 30c374def..6579ef7de 100644 --- a/xmake/core/base/string.lua +++ b/xmake/core/base/string.lua @@ -203,13 +203,13 @@ end -- serialize to string from the given object -- --- @param deflate deflate empty characters +-- @param opt serialize options +-- e.g. { strip = true, binary = false, indent = true } -- -- @return string, errors -- -function string.serialize(object, deflate) - deflate = not not deflate - return serialize.save(object, { strip = deflate, indent = not deflate }) +function string.serialize(object, opt) + return serialize.save(object, opt) end -- deserialize string to object -- cgit v1.3.1 From b724d37e01b4a12d1b5eea3a9a866ba4b8c4e46d Mon Sep 17 00:00:00 2001 From: OpportunityLiu Date: Thu, 25 Jul 2019 19:45:45 +0800 Subject: fix style --- tests/modules/string/serialize/test.lua | 4 +++- xmake/core/base/serialize.lua | 30 ++++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/tests/modules/string/serialize/test.lua b/tests/modules/string/serialize/test.lua index 1ab3852ba..567bee87f 100644 --- a/tests/modules/string/serialize/test.lua +++ b/tests/modules/string/serialize/test.lua @@ -3,7 +3,9 @@ function roundtrip(round0) local round1 = string.serialize(round0, false):deserialize() local round2 = string.serialize(round1, true):deserialize() local round3 = string.serialize(round2, {binary=true}):deserialize() - return round3 + local round4 = string.serialize(round3, {indent=16}):deserialize() + local round5 = string.serialize(round4, {indent=" \r\n\t"}):deserialize() + return round5 end function test_number(t) diff --git a/xmake/core/base/serialize.lua b/xmake/core/base/serialize.lua index 8b5b233b1..4a9392ebe 100644 --- a/xmake/core/base/serialize.lua +++ b/xmake/core/base/serialize.lua @@ -86,16 +86,12 @@ function serialize._maketable(object, opt, level) -- make indent local indent = "" if opt.indent then - indent = string.rep(" ", level) + indent = string.rep(opt.indent, level) end -- make head - local headstr - if opt.indent then - headstr = (level > 0 and "\n" or "") .. indent .. "{\n" - else - headstr = "{" - end + local headstr = opt.indent and "{\n" or "{" + -- make tail local tailstr if opt.indent then @@ -107,7 +103,7 @@ function serialize._maketable(object, opt, level) -- make body local s = {} if opt.indent then - indent = indent .. " " + indent = string.rep(opt.indent, level + 1) end if isarr then @@ -199,6 +195,24 @@ function serialize.save(object, opt) opt = { strip = false, binary = false, indent = true } end + if not opt.indent then + opt.indent = false + elseif type(opt.indent) == "boolean" then + opt.indent = " " + elseif type(opt.indent) == "number" then + if opt.indent < 0 then + opt.indent = false + else + opt.indent = string.rep(" ", opt.indent) + end + elseif type(opt.indent) == "string" then + if not opt.indent:match("^%s+$") then + return nil, "invalid opt.indent, only whitespaces are accepted" + end + else + return nil, "invalid opt.indent, should be boolean, number or string" + end + -- make string local result, errors = serialize._make(object, opt, 0) -- cgit v1.3.1 From 7d4023cf698bff82b2c0f2b4aad86286d4890b49 Mon Sep 17 00:00:00 2001 From: OpportunityLiu Date: Thu, 25 Jul 2019 21:27:00 +0800 Subject: add reference loop error --- xmake/core/base/serialize.lua | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/xmake/core/base/serialize.lua b/xmake/core/base/serialize.lua index 4a9392ebe..e9b435387 100644 --- a/xmake/core/base/serialize.lua +++ b/xmake/core/base/serialize.lua @@ -214,7 +214,15 @@ function serialize.save(object, opt) end -- make string - local result, errors = serialize._make(object, opt, 0) + local ok, result, errors = pcall(serialize._make, object, opt, 0) + + if not ok then + if result:find("stack overflow", 1, true) then + errors = "cannot serialize: reference loop found" + else + errors = "cannot serialize: " .. result + end + end -- ok? if errors ~= nil then @@ -226,11 +234,16 @@ function serialize.save(object, opt) end -- binary mode - local dump, lerr = serialize._dump(loadstring("return " .. result), true) + local func, lerr = loadstring("return " .. result) if lerr ~= nil then return nil, lerr end + local dump, derr = serialize._dump(func, true) + if derr ~= nil then + return nil, derr + end + -- return shorter representation return (#dump < #result) and dump or result end -- cgit v1.3.1