170 lines
5.0 KiB
Lua
170 lines
5.0 KiB
Lua
local tonumber = tonumber
|
|
|
|
local function 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 error('Expected ' .. delim) end
|
|
return pos, false
|
|
end
|
|
return pos + 1, true
|
|
end
|
|
|
|
-- A table of JSON->Lua escape characters
|
|
local esc_map = { b = '\b', f = '\f', n = '\n', r = '\r', t = '\t' }
|
|
|
|
local function parse_str_val(str, pos)
|
|
local out, n = {}, 0
|
|
if pos > #str then error("Malformed JSON (in string)") end
|
|
|
|
while true do
|
|
local c = str:sub(pos, pos)
|
|
if c == '"' then return table.concat(out, "", 1, n), pos + 1 end
|
|
|
|
n = n + 1
|
|
if c == '\\' then
|
|
local nextc = str:sub(pos + 1, pos + 1)
|
|
if not nextc then error("Malformed JSON (in string)") end
|
|
if nextc == "u" then
|
|
local num = tonumber(str:sub(pos + 2, pos + 5), 16)
|
|
if not num then error("Malformed JSON (in unicode string) ") end
|
|
if num <= 255 then
|
|
pos, out[n] = pos + 6, string.char(num)
|
|
else
|
|
pos, out[n] = pos + 6, "?"
|
|
end
|
|
else
|
|
pos, out[n] = pos + 2, esc_map[nextc] or nextc
|
|
end
|
|
else
|
|
pos, out[n] = pos + 1, c
|
|
end
|
|
end
|
|
end
|
|
|
|
local function parse_num_val(str, pos)
|
|
local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos)
|
|
local val = tonumber(num_str)
|
|
if not val then error('Error parsing number at position ' .. pos .. '.') end
|
|
return val, pos + #num_str
|
|
end
|
|
|
|
local null = {}
|
|
local literals = {['true'] = true, ['false'] = false, ['null'] = null }
|
|
|
|
-- Build a table of Lua->JSON escape characters
|
|
local escapes = {}
|
|
for i = 0, 255 do
|
|
local c = string.char(i)
|
|
if i >= 32 and i <= 126
|
|
then escapes[c] = c
|
|
else escapes[c] = ("\\u00%02x"):format(i)
|
|
end
|
|
end
|
|
escapes["\t"], escapes["\n"], escapes["\r"], escapes["\""], escapes["\\"] = "\\t", "\\n", "\\r", "\\\"", "\\\\"
|
|
|
|
local function parse(str, pos, end_delim)
|
|
pos = pos or 1
|
|
if pos > #str then error('Reached unexpected end of input.') end
|
|
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 = parse(str, pos, '}')
|
|
if key == nil then return obj, pos end
|
|
if not delim_found then error('Comma missing between object items.') end
|
|
pos = skip_delim(str, pos, ':', true)
|
|
obj[key], pos = parse(str, pos)
|
|
pos, delim_found = skip_delim(str, pos, ',')
|
|
end
|
|
elseif first == '[' then
|
|
local arr, val, delim_found = {}, true, true
|
|
pos = pos + 1
|
|
while true do
|
|
val, pos = parse(str, pos, ']')
|
|
if val == nil then return arr, pos end
|
|
if not delim_found then error('Comma missing between array items.') end
|
|
arr[#arr + 1] = val
|
|
pos, delim_found = skip_delim(str, pos, ',')
|
|
end
|
|
elseif first == '"' then
|
|
return parse_str_val(str, pos + 1)
|
|
elseif first == '-' or first:match('%d') then
|
|
return parse_num_val(str, pos)
|
|
elseif first == end_delim then
|
|
return nil, pos + 1
|
|
else
|
|
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)
|
|
error('Invalid json syntax starting at ' .. pos_info_str)
|
|
end
|
|
end
|
|
|
|
local format, gsub, tostring, pairs, next, type, concat
|
|
= string.format, string.gsub, tostring, pairs, next, type, table.concat
|
|
|
|
local function stringify_impl(t, out, n)
|
|
local ty = type(t)
|
|
if ty == "table" then
|
|
local first_ty = type(next(t))
|
|
if first_ty == "nil" then
|
|
-- Assume empty tables are arrays
|
|
out[n], n = "{}", n + 1
|
|
return n
|
|
elseif first_ty == "string" then
|
|
out[n], n = "{", n + 1
|
|
local first = true
|
|
for k, v in pairs(t) do
|
|
if first then first = false else out[n], n = ",", n + 1 end
|
|
out[n] = format("\"%s\":", k)
|
|
n = stringify_impl(v, out, n + 1)
|
|
end
|
|
out[n], n = "}", n + 1
|
|
return n
|
|
elseif first_ty == "number" then
|
|
out[n], n = "[", n + 1
|
|
for i = 1, #t do
|
|
if i > 1 then out[n], n = ",", n + 1 end
|
|
n = stringify_impl(t[i], out, n)
|
|
end
|
|
out[n], n = "]", n + 1
|
|
return n
|
|
else
|
|
error("Cannot serialize key " .. first_ty)
|
|
end
|
|
elseif ty == "string" then
|
|
if t:match("^[ -~]*$") then
|
|
out[n], n = gsub(format("%q", t), "\n", "n"), n + 1
|
|
else
|
|
out[n], n = "\"" .. gsub(t, ".", escapes) .. "\"", n + 1
|
|
end
|
|
return n
|
|
elseif ty == "number" or ty == "boolean" then
|
|
out[n],n = tostring(t), n + 1
|
|
return n
|
|
else error("Cannot serialize type " .. ty)
|
|
end
|
|
end
|
|
|
|
local function stringify(object)
|
|
local buffer = {}
|
|
local n = stringify_impl(object, buffer, 1)
|
|
return concat(buffer, "", 1, n - 1)
|
|
end
|
|
|
|
local function try_parse(msg)
|
|
local ok, res = pcall(parse, msg)
|
|
if ok then return res else return nil, res end
|
|
end
|
|
|
|
return {
|
|
stringify = stringify,
|
|
try_parse = try_parse,
|
|
parse = parse,
|
|
null = null
|
|
}
|