422 lines
8.9 KiB
Lua
422 lines
8.9 KiB
Lua
-- json.lua: High-performance JSON module for Moonshark
|
|
|
|
-- Pre-computed escape sequences to avoid recreating table
|
|
local escape_chars = {
|
|
['"'] = '\\"', ['\\'] = '\\\\',
|
|
['\n'] = '\\n', ['\r'] = '\\r', ['\t'] = '\\t'
|
|
}
|
|
|
|
function json_go_encode(value)
|
|
return __json_marshal(value)
|
|
end
|
|
|
|
function json_go_decode(str)
|
|
if type(str) ~= "string" then
|
|
error("json_decode: expected string, got " .. type(str), 2)
|
|
end
|
|
return __json_unmarshal(str)
|
|
end
|
|
|
|
function json_encode(data)
|
|
local t = type(data)
|
|
|
|
if t == "nil" then return "null" end
|
|
if t == "boolean" then return data and "true" or "false" end
|
|
if t == "number" then return tostring(data) end
|
|
|
|
if t == "string" then
|
|
return '"' .. data:gsub('[\\"\n\r\t]', escape_chars) .. '"'
|
|
end
|
|
|
|
if t == "table" then
|
|
local isArray = true
|
|
local count = 0
|
|
|
|
-- Check if it's an array in one pass
|
|
for k, _ in pairs(data) do
|
|
count = count + 1
|
|
if type(k) ~= "number" or k ~= count or k < 1 then
|
|
isArray = false
|
|
break
|
|
end
|
|
end
|
|
|
|
if isArray then
|
|
local result = {}
|
|
for i = 1, count do
|
|
result[i] = json_encode(data[i])
|
|
end
|
|
return "[" .. table.concat(result, ",") .. "]"
|
|
else
|
|
local result = {}
|
|
local index = 1
|
|
for k, v in pairs(data) do
|
|
if type(k) == "string" and type(v) ~= "function" and type(v) ~= "userdata" then
|
|
result[index] = json_encode(k) .. ":" .. json_encode(v)
|
|
index = index + 1
|
|
end
|
|
end
|
|
return "{" .. table.concat(result, ",") .. "}"
|
|
end
|
|
end
|
|
|
|
return "null" -- Unsupported type
|
|
end
|
|
|
|
function json_decode(data)
|
|
local pos = 1
|
|
local len = #data
|
|
|
|
-- Pre-compute byte values
|
|
local b_space = string.byte(' ')
|
|
local b_tab = string.byte('\t')
|
|
local b_cr = string.byte('\r')
|
|
local b_lf = string.byte('\n')
|
|
local b_quote = string.byte('"')
|
|
local b_backslash = string.byte('\\')
|
|
local b_slash = string.byte('/')
|
|
local b_lcurly = string.byte('{')
|
|
local b_rcurly = string.byte('}')
|
|
local b_lbracket = string.byte('[')
|
|
local b_rbracket = string.byte(']')
|
|
local b_colon = string.byte(':')
|
|
local b_comma = string.byte(',')
|
|
local b_0 = string.byte('0')
|
|
local b_9 = string.byte('9')
|
|
local b_minus = string.byte('-')
|
|
local b_plus = string.byte('+')
|
|
local b_dot = string.byte('.')
|
|
local b_e = string.byte('e')
|
|
local b_E = string.byte('E')
|
|
|
|
-- Skip whitespace more efficiently
|
|
local function skip()
|
|
local b
|
|
while pos <= len do
|
|
b = data:byte(pos)
|
|
if b > b_space or (b ~= b_space and b ~= b_tab and b ~= b_cr and b ~= b_lf) then
|
|
break
|
|
end
|
|
pos = pos + 1
|
|
end
|
|
end
|
|
|
|
-- Forward declarations
|
|
local parse_value, parse_string, parse_number, parse_object, parse_array
|
|
|
|
-- Parse a string more efficiently
|
|
parse_string = function()
|
|
pos = pos + 1 -- Skip opening quote
|
|
|
|
if pos > len then
|
|
error("Unterminated string")
|
|
end
|
|
|
|
-- Use a table to build the string
|
|
local result = {}
|
|
local result_pos = 1
|
|
local start = pos
|
|
local c, b
|
|
|
|
while pos <= len do
|
|
b = data:byte(pos)
|
|
|
|
if b == b_backslash then
|
|
-- Add the chunk before the escape character
|
|
if pos > start then
|
|
result[result_pos] = data:sub(start, pos - 1)
|
|
result_pos = result_pos + 1
|
|
end
|
|
|
|
pos = pos + 1
|
|
if pos > len then
|
|
error("Unterminated string escape")
|
|
end
|
|
|
|
c = data:byte(pos)
|
|
if c == b_quote then
|
|
result[result_pos] = '"'
|
|
elseif c == b_backslash then
|
|
result[result_pos] = '\\'
|
|
elseif c == b_slash then
|
|
result[result_pos] = '/'
|
|
elseif c == string.byte('b') then
|
|
result[result_pos] = '\b'
|
|
elseif c == string.byte('f') then
|
|
result[result_pos] = '\f'
|
|
elseif c == string.byte('n') then
|
|
result[result_pos] = '\n'
|
|
elseif c == string.byte('r') then
|
|
result[result_pos] = '\r'
|
|
elseif c == string.byte('t') then
|
|
result[result_pos] = '\t'
|
|
else
|
|
result[result_pos] = data:sub(pos, pos)
|
|
end
|
|
|
|
result_pos = result_pos + 1
|
|
pos = pos + 1
|
|
start = pos
|
|
elseif b == b_quote then
|
|
-- Add the final chunk
|
|
if pos > start then
|
|
result[result_pos] = data:sub(start, pos - 1)
|
|
result_pos = result_pos + 1
|
|
end
|
|
|
|
pos = pos + 1
|
|
return table.concat(result)
|
|
else
|
|
pos = pos + 1
|
|
end
|
|
end
|
|
|
|
error("Unterminated string")
|
|
end
|
|
|
|
-- Parse a number more efficiently
|
|
parse_number = function()
|
|
local start = pos
|
|
local b = data:byte(pos)
|
|
|
|
-- Skip any sign
|
|
if b == b_minus then
|
|
pos = pos + 1
|
|
if pos > len then
|
|
error("Malformed number")
|
|
end
|
|
b = data:byte(pos)
|
|
end
|
|
|
|
-- Integer part
|
|
if b < b_0 or b > b_9 then
|
|
error("Malformed number")
|
|
end
|
|
|
|
repeat
|
|
pos = pos + 1
|
|
if pos > len then break end
|
|
b = data:byte(pos)
|
|
until b < b_0 or b > b_9
|
|
|
|
-- Fractional part
|
|
if pos <= len and b == b_dot then
|
|
pos = pos + 1
|
|
if pos > len or data:byte(pos) < b_0 or data:byte(pos) > b_9 then
|
|
error("Malformed number")
|
|
end
|
|
|
|
repeat
|
|
pos = pos + 1
|
|
if pos > len then break end
|
|
b = data:byte(pos)
|
|
until b < b_0 or b > b_9
|
|
end
|
|
|
|
-- Exponent
|
|
if pos <= len and (b == b_e or b == b_E) then
|
|
pos = pos + 1
|
|
if pos > len then
|
|
error("Malformed number")
|
|
end
|
|
|
|
b = data:byte(pos)
|
|
if b == b_plus or b == b_minus then
|
|
pos = pos + 1
|
|
if pos > len then
|
|
error("Malformed number")
|
|
end
|
|
b = data:byte(pos)
|
|
end
|
|
|
|
if b < b_0 or b > b_9 then
|
|
error("Malformed number")
|
|
end
|
|
|
|
repeat
|
|
pos = pos + 1
|
|
if pos > len then break end
|
|
b = data:byte(pos)
|
|
until b < b_0 or b > b_9
|
|
end
|
|
|
|
return tonumber(data:sub(start, pos - 1))
|
|
end
|
|
|
|
-- Parse an object more efficiently
|
|
parse_object = function()
|
|
pos = pos + 1 -- Skip opening brace
|
|
local obj = {}
|
|
|
|
skip()
|
|
if pos <= len and data:byte(pos) == b_rcurly then
|
|
pos = pos + 1
|
|
return obj
|
|
end
|
|
|
|
while pos <= len do
|
|
skip()
|
|
|
|
if data:byte(pos) ~= b_quote then
|
|
error("Expected string key")
|
|
end
|
|
|
|
local key = parse_string()
|
|
skip()
|
|
|
|
if data:byte(pos) ~= b_colon then
|
|
error("Expected colon")
|
|
end
|
|
pos = pos + 1
|
|
|
|
obj[key] = parse_value()
|
|
skip()
|
|
|
|
local b = data:byte(pos)
|
|
if b == b_rcurly then
|
|
pos = pos + 1
|
|
return obj
|
|
end
|
|
|
|
if b ~= b_comma then
|
|
error("Expected comma or closing brace")
|
|
end
|
|
pos = pos + 1
|
|
end
|
|
|
|
error("Unterminated object")
|
|
end
|
|
|
|
-- Parse an array more efficiently
|
|
parse_array = function()
|
|
pos = pos + 1 -- Skip opening bracket
|
|
local arr = {}
|
|
local index = 1
|
|
|
|
skip()
|
|
if pos <= len and data:byte(pos) == b_rbracket then
|
|
pos = pos + 1
|
|
return arr
|
|
end
|
|
|
|
while pos <= len do
|
|
arr[index] = parse_value()
|
|
index = index + 1
|
|
|
|
skip()
|
|
|
|
local b = data:byte(pos)
|
|
if b == b_rbracket then
|
|
pos = pos + 1
|
|
return arr
|
|
end
|
|
|
|
if b ~= b_comma then
|
|
error("Expected comma or closing bracket")
|
|
end
|
|
pos = pos + 1
|
|
end
|
|
|
|
error("Unterminated array")
|
|
end
|
|
|
|
-- Parse a value more efficiently
|
|
parse_value = function()
|
|
skip()
|
|
|
|
if pos > len then
|
|
error("Unexpected end of input")
|
|
end
|
|
|
|
local b = data:byte(pos)
|
|
|
|
if b == b_quote then
|
|
return parse_string()
|
|
elseif b == b_lcurly then
|
|
return parse_object()
|
|
elseif b == b_lbracket then
|
|
return parse_array()
|
|
elseif b == string.byte('n') and pos + 3 <= len and data:sub(pos, pos + 3) == "null" then
|
|
pos = pos + 4
|
|
return nil
|
|
elseif b == string.byte('t') and pos + 3 <= len and data:sub(pos, pos + 3) == "true" then
|
|
pos = pos + 4
|
|
return true
|
|
elseif b == string.byte('f') and pos + 4 <= len and data:sub(pos, pos + 4) == "false" then
|
|
pos = pos + 5
|
|
return false
|
|
elseif b == b_minus or (b >= b_0 and b <= b_9) then
|
|
return parse_number()
|
|
else
|
|
error("Unexpected character: " .. string.char(b))
|
|
end
|
|
end
|
|
|
|
skip()
|
|
local result = parse_value()
|
|
skip()
|
|
|
|
if pos <= len then
|
|
error("Unexpected trailing characters")
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
function json_is_valid(str)
|
|
if type(str) ~= "string" then return false end
|
|
local status, _ = pcall(json_decode, str)
|
|
return status
|
|
end
|
|
|
|
function json_pretty_print(value)
|
|
if type(value) == "string" then
|
|
value = json_decode(value)
|
|
end
|
|
|
|
local function stringify(val, indent, visited)
|
|
visited = visited or {}
|
|
indent = indent or 0
|
|
local spaces = string.rep(" ", indent)
|
|
|
|
if type(val) == "table" then
|
|
if visited[val] then return "{...}" end
|
|
visited[val] = true
|
|
|
|
local isArray = true
|
|
local i = 1
|
|
for k in pairs(val) do
|
|
if type(k) ~= "number" or k ~= i then
|
|
isArray = false
|
|
break
|
|
end
|
|
i = i + 1
|
|
end
|
|
|
|
local result = isArray and "[\n" or "{\n"
|
|
local first = true
|
|
|
|
if isArray then
|
|
for i, v in ipairs(val) do
|
|
if not first then result = result .. ",\n" end
|
|
first = false
|
|
result = result .. spaces .. " " .. stringify(v, indent + 1, visited)
|
|
end
|
|
else
|
|
for k, v in pairs(val) do
|
|
if not first then result = result .. ",\n" end
|
|
first = false
|
|
result = result .. spaces .. " \"" .. tostring(k) .. "\": " .. stringify(v, indent + 1, visited)
|
|
end
|
|
end
|
|
|
|
return result .. "\n" .. spaces .. (isArray and "]" or "}")
|
|
elseif type(val) == "string" then
|
|
return "\"" .. val:gsub('\\', '\\\\'):gsub('"', '\\"'):gsub('\n', '\\n') .. "\""
|
|
else
|
|
return tostring(val)
|
|
end
|
|
end
|
|
|
|
return stringify(value)
|
|
end |