--!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-present, Xmake Open Source Community. -- -- @author ruki -- @file json.lua -- -- define module: json local json = json or {} -- load modules local io = require("base/io") local os = require("base/os") local table = require("base/table") local utils = require("base/utils") -- export null json.purenull = {} setmetatable(json.purenull, { __is_json_null = true, __eq = function (obj) if type(obj) == "table" then local mt = getmetatable(obj) if mt and mt.__is_json_null then return true end end return false end, __tostring = function() return "null" end}) if cjson then json.null = cjson.null else json.null = json.purenull end function json._pure_kind_of(obj) if type(obj) ~= "table" then return type(obj) end if json.is_marked_as_array(obj) then return "array" end if obj == json.purenull then return "nil" end local i = 1 for _ in pairs(obj) do if obj[i] ~= nil then i = i + 1 else return "table" end end if i == 1 then return "table" else return "array" end end function json._pure_escape_str(s) local in_char = {'\\', '"', '/', '\b', '\f', '\n', '\r', '\t'} local out_char = {'\\', '"', '/', 'b', 'f', 'n', 'r', 't'} for i, c in ipairs(in_char) do s = s:gsub(c, '\\' .. out_char[i]) end return s end function json._pure_skip_delim(str, pos, delim, err_if_missing) pos = pos + #str:match('^%s*', pos) if str:sub(pos, pos) ~= delim then if err_if_missing then os.raise("expected %s near position %d", delim, pos) end return pos, false end return pos + 1, true end function json._pure_parse_str_val(str, pos, val) val = val or '' local early_end_error = "end of input found while parsing string." if pos > #str then os.raise(early_end_error) end local c = str:sub(pos, pos) if c == '"' then return val, pos + 1 end if c ~= '\\' then return json._pure_parse_str_val(str, pos + 1, val .. c) end -- we must have a \ character. local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'} local nextc = str:sub(pos + 1, pos + 1) if not nextc then os.raise(early_end_error) end return json._pure_parse_str_val(str, pos + 2, val .. (esc_map[nextc] or nextc)) end function json._pure_parse_num_val(str, pos) local num_str if str:sub(pos, pos + 1) == "0x" then num_str = str:match('^-?0[xX][0-9a-fA-F]+', pos) else num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos) end local val = tonumber(num_str) if not val then os.raise("error parsing number at position %d", pos) end return val, pos + #num_str end function json._pure_stringify(obj, level, as_key, opt) opt = opt or {} level = level or 0 local pretty = opt.pretty local orderkeys = opt.orderkeys if orderkeys == nil and pretty then orderkeys = true end local indent_step = pretty and (opt.indent or 4) or nil local newline = pretty and "\n" or "" local curr_indent = indent_step and string.rep(" ", indent_step * level) or nil local child_indent = indent_step and string.rep(" ", indent_step * (level + 1)) or nil local s = {} local kind = json._pure_kind_of(obj) if kind == "array" then if as_key then os.raise("can\'t encode array as key.") end s[#s + 1] = '[' local arrlen = #obj if pretty and arrlen > 0 then s[#s + 1] = newline end for idx = 1, arrlen do if idx > 1 then s[#s + 1] = ',' if pretty then s[#s + 1] = newline end end if pretty then s[#s + 1] = child_indent end s[#s + 1] = json._pure_stringify(obj[idx], level + 1, false, opt) end if pretty and arrlen > 0 then s[#s + 1] = newline s[#s + 1] = curr_indent end s[#s + 1] = ']' elseif kind == "table" then if as_key then os.raise("can\'t encode table as key.") end s[#s + 1] = '{' local first = true local iter = orderkeys and table.orderpairs or pairs for k, v in iter(obj) do if not first then s[#s + 1] = ',' end if pretty then s[#s + 1] = newline s[#s + 1] = child_indent end s[#s + 1] = json._pure_stringify(k, level + 1, true, opt) s[#s + 1] = ':' if pretty then s[#s + 1] = ' ' end s[#s + 1] = json._pure_stringify(v, level + 1, false, opt) first = false end if pretty and not first then s[#s + 1] = newline s[#s + 1] = curr_indent end s[#s + 1] = '}' elseif kind == "string" then return '"' .. json._pure_escape_str(obj) .. '"' elseif kind == "number" then if as_key then return '"' .. tostring(obj) .. '"' end return tostring(obj) elseif kind == "boolean" then return tostring(obj) elseif kind == "nil" then return "null" else os.raise("unknown type: %s", kind) end return table.concat(s) end function json._pure_parse(str, pos, end_delim) pos = pos or 1 if pos > #str then os.raise("reached unexpected end of input.") end -- skip whitespace. local pos = pos + #str:match('^%s*', pos) local first = str:sub(pos, pos) if first == '{' then local obj, key, delim_found = {}, true, true pos = pos + 1 while true do key, pos = json._pure_parse(str, pos, '}') if key == nil then return obj, pos end if not delim_found then os.raise("comma missing between object items.") end pos = json._pure_skip_delim(str, pos, ':', true) -- true -> error if missing. obj[key], pos = json._pure_parse(str, pos) pos, delim_found = json._pure_skip_delim(str, pos, ',') end elseif first == '[' then local arr, val, delim_found = {}, true, true json.mark_as_array(arr) pos = pos + 1 while true do val, pos = json._pure_parse(str, pos, ']') if val == nil then return arr, pos end if not delim_found then os.raise("comma missing between array items.") end arr[#arr + 1] = val pos, delim_found = json._pure_skip_delim(str, pos, ',') end elseif first == '"' then return json._pure_parse_str_val(str, pos + 1) elseif first == '-' or first:match("%d") then return json._pure_parse_num_val(str, pos) elseif first == end_delim then -- end of an object or array. return nil, pos + 1 else local literals = {["true"] = true, ["false"] = false, ["null"] = json.purenull} for lit_str, lit_val in pairs(literals) do local lit_end = pos + #lit_str - 1 if str:sub(pos, lit_end) == lit_str then return lit_val, lit_end + 1 end end local pos_info_str = "position " .. pos .. ": " .. str:sub(pos, pos + 10) os.raise("invalid json syntax starting at " .. pos_info_str) end end -- decode json string using pure lua function json._pure_decode(jsonstr, opt) return json._pure_parse(jsonstr) end -- encode json string using pua lua function json._pure_encode(luatable, opt) return json._pure_stringify(luatable, 0, false, opt) end -- support empty array -- @see https://github.com/mpx/lua-cjson/issues/11 function json.mark_as_array(luatable) local mt = getmetatable(luatable) or {} mt.__is_cjson_array = true return setmetatable(luatable, mt) end -- is marked as array? function json.is_marked_as_array(luatable) local mt = getmetatable(luatable) return mt and mt.__is_cjson_array end -- decode json string to the lua table -- -- @param jsonstr the json string -- @param opt the options -- -- @return the lua table -- function json.decode(jsonstr, opt) local decode = cjson and cjson.decode or json._pure_decode if opt and opt.pure then decode = json._pure_decode end local ok, luatable_or_errors = utils.trycall(decode, nil, jsonstr) if not ok then return nil, string.format("decode json failed, %s", luatable_or_errors) end return luatable_or_errors end -- encode lua table to the json string -- -- @param luatable the lua table -- @param opt the options -- -- @return the json string -- function json.encode(luatable, opt) local use_pure = not cjson or (opt and (opt.pure or opt.pretty)) local encode = use_pure and json._pure_encode or cjson.encode local ok, jsonstr_or_errors if use_pure then ok, jsonstr_or_errors = utils.trycall(encode, nil, luatable, opt) else ok, jsonstr_or_errors = utils.trycall(encode, nil, luatable) end if not ok then return nil, string.format("encode json failed, %s", jsonstr_or_errors) end return jsonstr_or_errors end -- load json file to the lua table -- -- @param filepath the json file path -- @param opt the options -- - encoding for io/file, e.g. utf8, utf16, utf16le, utf16be .. -- - continuation for io/read (concat string with the given continuation characters) -- -- @return the lua table -- function json.loadfile(filepath, opt) local filedata, errors = io.readfile(filepath, opt) if not filedata then return nil, errors end return json.decode(filedata, opt) end -- save lua table to the json file -- -- @param filepath the json file path -- @param luatable the lua table -- @param opt the options -- - encoding for io/file, e.g. utf8, utf16, utf16le, utf16be .. -- -- @return the json string -- function json.savefile(filepath, luatable, opt) local jsonstr, errors = json.encode(luatable, opt) if not jsonstr then return false, errors end return io.writefile(filepath, jsonstr, opt) end -- return module: json return json