diff --git a/core/runner/embed.go b/core/runner/embed.go index 6371a08..780158d 100644 --- a/core/runner/embed.go +++ b/core/runner/embed.go @@ -13,10 +13,15 @@ import ( //go:embed sandbox.lua var sandboxLuaCode string +//go:embed json.lua +var jsonLuaCode string + // Global bytecode cache to improve performance var ( - sandboxBytecode atomic.Pointer[[]byte] - bytecodeOnce sync.Once + sandboxBytecode atomic.Pointer[[]byte] + jsonBytecode atomic.Pointer[[]byte] + bytecodeOnce sync.Once + jsonBytecodeOnce sync.Once ) // precompileSandboxCode compiles the sandbox.lua code to bytecode once @@ -41,10 +46,64 @@ func precompileSandboxCode() { logger.Debug("Successfully precompiled sandbox.lua to bytecode (%d bytes)", len(code)) } +// precompileJsonModule compiles the json.lua code to bytecode once +func precompileJsonModule() { + tempState := luajit.New() + if tempState == nil { + logger.Fatal("Failed to create temp Lua state for JSON module compilation") + } + defer tempState.Close() + defer tempState.Cleanup() + + code, err := tempState.CompileBytecode(jsonLuaCode, "json.lua") + if err != nil { + logger.Error("Failed to compile JSON module: %v", err) + return + } + + bytecode := make([]byte, len(code)) + copy(bytecode, code) + jsonBytecode.Store(&bytecode) + + logger.Debug("Successfully precompiled json.lua to bytecode (%d bytes)", len(code)) +} + // loadSandboxIntoState loads the sandbox code into a Lua state func loadSandboxIntoState(state *luajit.State, verbose bool) error { bytecodeOnce.Do(precompileSandboxCode) + jsonBytecodeOnce.Do(precompileJsonModule) + // First load and execute the JSON module + jsBytecode := jsonBytecode.Load() + if jsBytecode != nil && len(*jsBytecode) > 0 { + if verbose { + logger.Debug("Loading json.lua from precompiled bytecode") + } + + // Execute JSON module bytecode and store result to _G.json + if err := state.LoadBytecode(*jsBytecode, "json.lua"); err != nil { + return err + } + + // Execute with 1 result and store to _G.json + if err := state.RunBytecodeWithResults(1); err != nil { + return err + } + + // The module table is now on top of stack, set it to _G.json + state.SetGlobal("json") + } else { + // Fallback - compile and execute JSON module directly + if verbose { + logger.Warning("Using non-precompiled json.lua") + } + + if err := state.DoString(jsonLuaCode); err != nil { + return err + } + } + + // Then load sandbox bytecode := sandboxBytecode.Load() if bytecode != nil && len(*bytecode) > 0 { if verbose { @@ -53,9 +112,9 @@ func loadSandboxIntoState(state *luajit.State, verbose bool) error { return state.LoadAndRunBytecode(*bytecode, "sandbox.lua") } - // Fallback to direct execution + // Fallback if verbose { - logger.Warning("Using non-precompiled sandbox.lua (bytecode compilation failed)") + logger.Warning("Using non-precompiled sandbox.lua") } return state.DoString(sandboxLuaCode) } diff --git a/core/runner/json.lua b/core/runner/json.lua new file mode 100644 index 0000000..5d0aa67 --- /dev/null +++ b/core/runner/json.lua @@ -0,0 +1,433 @@ +-- json.lua: High-performance JSON module for Moonshark +local json = {} + +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 + local escape_chars = { + ['"'] = '\\"', ['\\'] = '\\\\', + ['\n'] = '\\n', ['\r'] = '\\r', ['\t'] = '\\t' + } + return '"' .. data:gsub('[\\"\n\r\t]', escape_chars) .. '"' + end + + if t == "table" then + local isArray = true + local count = 0 + local max_index = 0 + + for k, _ in pairs(data) do + count = count + 1 + if type(k) == "number" and k > 0 and math.floor(k) == k then + max_index = math.max(max_index, k) + else + isArray = false + break + end + end + + local result = {} + + if isArray then + for i, v in ipairs(data) do + result[i] = json.encode(v) + end + return "[" .. table.concat(result, ",") .. "]" + else + local size = 0 + for k, v in pairs(data) do + if type(k) == "string" and type(v) ~= "function" and type(v) ~= "userdata" then + size = size + 1 + end + end + + 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(json) + local pos = 1 + local len = #json + + -- 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 = json: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 = json:byte(pos) + + if b == b_backslash then + -- Add the chunk before the escape character + if pos > start then + result[result_pos] = json:sub(start, pos - 1) + result_pos = result_pos + 1 + end + + pos = pos + 1 + if pos > len then + error("Unterminated string escape") + end + + c = json: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] = json: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] = json: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 = json:byte(pos) + + -- Skip any sign + if b == b_minus then + pos = pos + 1 + if pos > len then + error("Malformed number") + end + b = json: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 = json: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 json:byte(pos) < b_0 or json:byte(pos) > b_9 then + error("Malformed number") + end + + repeat + pos = pos + 1 + if pos > len then break end + b = json: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 = json:byte(pos) + if b == b_plus or b == b_minus then + pos = pos + 1 + if pos > len then + error("Malformed number") + end + b = json: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 = json:byte(pos) + until b < b_0 or b > b_9 + end + + return tonumber(json: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 json:byte(pos) == b_rcurly then + pos = pos + 1 + return obj + end + + while pos <= len do + skip() + + if json:byte(pos) ~= b_quote then + error("Expected string key") + end + + local key = parse_string() + skip() + + if json:byte(pos) ~= b_colon then + error("Expected colon") + end + pos = pos + 1 + + obj[key] = parse_value() + skip() + + local b = json: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 json: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 = json: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 = json: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 json:sub(pos, pos + 3) == "null" then + pos = pos + 4 + return nil + elseif b == string.byte('t') and pos + 3 <= len and json:sub(pos, pos + 3) == "true" then + pos = pos + 4 + return true + elseif b == string.byte('f') and pos + 4 <= len and json: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 + +return json diff --git a/core/runner/sandbox.lua b/core/runner/sandbox.lua index d9e81b8..9568437 100644 --- a/core/runner/sandbox.lua +++ b/core/runner/sandbox.lua @@ -386,14 +386,6 @@ local util = { return __generate_token(length or 32) end, - json_encode = function(string) - return __json_marshal(string or "{}") - end, - - json_decode = function(string) - return __json_unmarshal(string or "{}") - end, - -- Deep copy of tables deep_copy = function(obj) if type(obj) ~= 'table' then return obj end