diff --git a/runner/embed.go b/runner/embed.go index 655d44f..8cb47c7 100644 --- a/runner/embed.go +++ b/runner/embed.go @@ -40,6 +40,21 @@ var mathLuaCode string //go:embed lua/env.lua var envLuaCode string +//go:embed lua/http.lua +var httpLuaCode string + +//go:embed lua/cookie.lua +var cookieLuaCode string + +//go:embed lua/csrf.lua +var csrfLuaCode string + +//go:embed lua/render.lua +var renderLuaCode string + +//go:embed lua/session.lua +var sessionLuaCode string + // Module represents a Lua module to load type Module struct { name string @@ -48,6 +63,11 @@ type Module struct { } var modules = []Module{ + {"http", httpLuaCode, true}, + {"cookie", cookieLuaCode, true}, + {"session", sessionLuaCode, true}, + {"csrf", csrfLuaCode, true}, + {"render", renderLuaCode, true}, {"json", jsonLuaCode, true}, {"sqlite", sqliteLuaCode, false}, {"fs", fsLuaCode, true}, diff --git a/runner/lua/cookie.lua b/runner/lua/cookie.lua new file mode 100644 index 0000000..f9c9483 --- /dev/null +++ b/runner/lua/cookie.lua @@ -0,0 +1,26 @@ +-- cookie.lua + +function cookie_set(name, value, options) + __response.cookies = __response.cookies or {} + local opts = options or {} + local cookie = { + name = name, + value = value or "", + path = opts.path or "/", + domain = opts.domain, + secure = opts.secure ~= false, + http_only = opts.http_only ~= false + } + if opts.expires and opts.expires > 0 then + cookie.max_age = opts.expires + end + table.insert(__response.cookies, cookie) +end + +function cookie_get(name) + return __ctx._request_cookies and __ctx._request_cookies[name] +end + +function cookie_delete(name, path, domain) + return cookie_set(name, "", {expires = -1, path = path or "/", domain = domain}) +end diff --git a/runner/lua/crypto.lua b/runner/lua/crypto.lua index 238cf86..c25f6d2 100644 --- a/runner/lua/crypto.lua +++ b/runner/lua/crypto.lua @@ -1,6 +1,4 @@ ---[[ -crypto.lua - Cryptographic functions powered by Go -]]-- +-- crypto.lua -- ====================================================================== -- HASHING FUNCTIONS @@ -13,10 +11,10 @@ function hash(data, algorithm, format) if type(data) ~= "string" then error("hash: data must be a string", 2) end - + algorithm = algorithm or "sha256" format = format or "hex" - + return __crypto_hash(data, algorithm, format) end @@ -47,14 +45,14 @@ function hmac(data, key, algorithm, format) if type(data) ~= "string" then error("hmac: data must be a string", 2) end - + if type(key) ~= "string" then error("hmac: key must be a string", 2) end - + algorithm = algorithm or "sha256" format = format or "hex" - + return __crypto_hmac(data, key, algorithm, format) end @@ -84,10 +82,10 @@ function random_bytes(length, secure, format) if type(length) ~= "number" or length <= 0 then error("random_bytes: length must be positive", 2) end - + secure = secure ~= false -- Default to secure format = format or "binary" - + return __crypto_random_bytes(length, secure, format) end @@ -96,13 +94,13 @@ function random_int(min, max, secure) if type(min) ~= "number" or type(max) ~= "number" then error("random_int: min and max must be numbers", 2) end - + if max <= min then error("random_int: max must be greater than min", 2) end - + secure = secure ~= false -- Default to secure - + return __crypto_random_int(min, max, secure) end @@ -111,24 +109,24 @@ function random_string(length, charset, secure) if type(length) ~= "number" or length <= 0 then error("random_string: length must be positive", 2) end - + secure = secure ~= false -- Default to secure - + -- Default character set: alphanumeric charset = charset or "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" - + if type(charset) ~= "string" or #charset == 0 then error("random_string: charset must be non-empty", 2) end - + local result = "" local charset_length = #charset - + for i = 1, length do local index = random_int(1, charset_length, secure) result = result .. charset:sub(index, index) end - + return result end @@ -139,4 +137,4 @@ end -- Generate random UUID (v4) function uuid() return __crypto_uuid() -end \ No newline at end of file +end diff --git a/runner/lua/csrf.lua b/runner/lua/csrf.lua new file mode 100644 index 0000000..2bac32a --- /dev/null +++ b/runner/lua/csrf.lua @@ -0,0 +1,33 @@ +-- csrf.lua + +function csrf_generate() + local token = generate_token(32) + session_set("_csrf_token", token) + return token +end + +function csrf_field() + local token = session_get("_csrf_token") + if not token then + token = csrf_generate() + end + return string.format('', + html_special_chars(token)) +end + +function csrf_validate() + local token = __ctx.session and __ctx.session.data and __ctx.session.data["_csrf_token"] + if not token then + __response.status = 403 + coroutine.yield("__EXIT__") + end + + local request_token = (__ctx._request_form and __ctx._request_form._csrf_token) or + (__ctx._request_headers and (__ctx._request_headers["x-csrf-token"] or __ctx._request_headers["csrf-token"])) + + if not request_token or request_token ~= token then + __response.status = 403 + coroutine.yield("__EXIT__") + end + return true +end diff --git a/runner/lua/fs.lua b/runner/lua/fs.lua index 7735f0c..b86d5ec 100644 --- a/runner/lua/fs.lua +++ b/runner/lua/fs.lua @@ -1,3 +1,5 @@ +-- fs.lua + function fs_read(path) if type(path) ~= "string" then error("fs_read: path must be a string", 2) @@ -131,4 +133,4 @@ function fs_write_json(path, data, pretty) end return fs_write(path, content) -end \ No newline at end of file +end diff --git a/runner/lua/http.lua b/runner/lua/http.lua new file mode 100644 index 0000000..0ca23b5 --- /dev/null +++ b/runner/lua/http.lua @@ -0,0 +1,72 @@ +-- http.lua + +function http_set_status(code) + __response.status = code +end + +function http_set_header(name, value) + __response.headers = __response.headers or {} + __response.headers[name] = value +end + +function http_set_content_type(ct) + __response.headers = __response.headers or {} + __response.headers["Content-Type"] = ct +end + +function http_set_metadata(key, value) + __response.metadata = __response.metadata or {} + __response.metadata[key] = value +end + +function http_redirect(url, status) + __response.status = status or 302 + __response.headers = __response.headers or {} + __response.headers["Location"] = url + coroutine.yield("__EXIT__") +end + +function send_html(content) + http_set_content_type("text/html") + return content +end + +function send_json(content) + http_set_content_type("application/json") + return content +end + +function send_text(content) + http_set_content_type("text/plain") + return content +end + +function send_xml(content) + http_set_content_type("application/xml") + return content +end + +function send_javascript(content) + http_set_content_type("application/javascript") + return content +end + +function send_css(content) + http_set_content_type("text/css") + return content +end + +function send_svg(content) + http_set_content_type("image/svg+xml") + return content +end + +function send_csv(content) + http_set_content_type("text/csv") + return content +end + +function send_binary(content, mime_type) + http_set_content_type(mime_type or "application/octet-stream") + return content +end diff --git a/runner/lua/json.lua b/runner/lua/json.lua index 8790463..7ea35ba 100644 --- a/runner/lua/json.lua +++ b/runner/lua/json.lua @@ -1,6 +1,5 @@ --- json.lua: High-performance JSON module for Moonshark +-- json.lua --- Pre-computed escape sequences to avoid recreating table local escape_chars = { ['"'] = '\\"', ['\\'] = '\\\\', ['\n'] = '\\n', ['\r'] = '\\r', ['\t'] = '\\t' @@ -419,4 +418,4 @@ function json_pretty_print(value) end return stringify(value) -end \ No newline at end of file +end diff --git a/runner/lua/math.lua b/runner/lua/math.lua index f801ae2..c48dca9 100644 --- a/runner/lua/math.lua +++ b/runner/lua/math.lua @@ -1,6 +1,4 @@ ---[[ -math.lua - High-performance math library -]]-- +-- math.lua local math_ext = {} diff --git a/runner/lua/render.lua b/runner/lua/render.lua new file mode 100644 index 0000000..d38d367 --- /dev/null +++ b/runner/lua/render.lua @@ -0,0 +1,190 @@ +-- render.lua + +-- Template processing with code execution +function render(template_str, env) + local function is_control_structure(code) + -- Check if code is a control structure that doesn't produce output + local trimmed = code:match("^%s*(.-)%s*$") + return trimmed == "else" or + trimmed == "end" or + trimmed:match("^if%s") or + trimmed:match("^elseif%s") or + trimmed:match("^for%s") or + trimmed:match("^while%s") or + trimmed:match("^repeat%s*$") or + trimmed:match("^until%s") or + trimmed:match("^do%s*$") or + trimmed:match("^local%s") or + trimmed:match("^function%s") or + trimmed:match(".*=%s*function%s*%(") or + trimmed:match(".*then%s*$") or + trimmed:match(".*do%s*$") + end + + local pos, chunks = 1, {} + while pos <= #template_str do + local unescaped_start = template_str:find("{{{", pos, true) + local escaped_start = template_str:find("{{", pos, true) + + local start, tag_type, open_len + if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then + start, tag_type, open_len = unescaped_start, "-", 3 + elseif escaped_start then + start, tag_type, open_len = escaped_start, "=", 2 + else + table.insert(chunks, template_str:sub(pos)) + break + end + + if start > pos then + table.insert(chunks, template_str:sub(pos, start-1)) + end + + pos = start + open_len + local close_tag = tag_type == "-" and "}}}" or "}}" + local close_start, close_stop = template_str:find(close_tag, pos, true) + if not close_start then + error("Failed to find closing tag at position " .. pos) + end + + local code = template_str:sub(pos, close_start-1):match("^%s*(.-)%s*$") + local is_control = is_control_structure(code) + + table.insert(chunks, {tag_type, code, pos, is_control}) + pos = close_stop + 1 + end + + local buffer = {"local _tostring, _escape, _b, _b_i = ...\n"} + for _, chunk in ipairs(chunks) do + local t = type(chunk) + if t == "string" then + table.insert(buffer, "_b_i = _b_i + 1\n") + table.insert(buffer, "_b[_b_i] = " .. string.format("%q", chunk) .. "\n") + else + local tag_type, code, pos, is_control = chunk[1], chunk[2], chunk[3], chunk[4] + + if is_control then + -- Control structure - just insert as raw Lua code + table.insert(buffer, "--[[" .. pos .. "]] " .. code .. "\n") + elseif tag_type == "=" then + -- Simple variable check + if code:match("^[%w_]+$") then + table.insert(buffer, "_b_i = _b_i + 1\n") + table.insert(buffer, "--[[" .. pos .. "]] _b[_b_i] = _escape(_tostring(" .. code .. "))\n") + else + -- Expression output with escaping + table.insert(buffer, "_b_i = _b_i + 1\n") + table.insert(buffer, "--[[" .. pos .. "]] _b[_b_i] = _escape(_tostring(" .. code .. "))\n") + end + elseif tag_type == "-" then + -- Unescaped output + table.insert(buffer, "_b_i = _b_i + 1\n") + table.insert(buffer, "--[[" .. pos .. "]] _b[_b_i] = _tostring(" .. code .. ")\n") + end + end + end + table.insert(buffer, "return _b") + + local generated_code = table.concat(buffer) + + -- DEBUG: Uncomment to see generated code + -- print("Generated Lua code:") + -- print(generated_code) + -- print("---") + + local fn, err = loadstring(generated_code) + if not fn then + print("Generated code that failed to compile:") + print(generated_code) + error(err) + end + + env = env or {} + local runtime_env = setmetatable({}, {__index = function(_, k) return env[k] or _G[k] end}) + setfenv(fn, runtime_env) + + local output_buffer = {} + fn(tostring, html_special_chars, output_buffer, 0) + return table.concat(output_buffer) +end + +-- Named placeholder processing +function parse(template_str, env) + local pos, output = 1, {} + env = env or {} + + while pos <= #template_str do + local unescaped_start, unescaped_end, unescaped_name = template_str:find("{{{%s*([%w_]+)%s*}}}", pos) + local escaped_start, escaped_end, escaped_name = template_str:find("{{%s*([%w_]+)%s*}}", pos) + + local next_pos, placeholder_end, name, escaped + if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then + next_pos, placeholder_end, name, escaped = unescaped_start, unescaped_end, unescaped_name, false + elseif escaped_start then + next_pos, placeholder_end, name, escaped = escaped_start, escaped_end, escaped_name, true + else + local text = template_str:sub(pos) + if text and #text > 0 then + table.insert(output, text) + end + break + end + + local text = template_str:sub(pos, next_pos - 1) + if text and #text > 0 then + table.insert(output, text) + end + + local value = env[name] + local str = tostring(value or "") + if escaped then + str = html_special_chars(str) + end + table.insert(output, str) + + pos = placeholder_end + 1 + end + + return table.concat(output) +end + +-- Indexed placeholder processing +function iparse(template_str, values) + local pos, output, value_index = 1, {}, 1 + values = values or {} + + while pos <= #template_str do + local unescaped_start, unescaped_end = template_str:find("{{{}}}", pos, true) + local escaped_start, escaped_end = template_str:find("{{}}", pos, true) + + local next_pos, placeholder_end, escaped + if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then + next_pos, placeholder_end, escaped = unescaped_start, unescaped_end, false + elseif escaped_start then + next_pos, placeholder_end, escaped = escaped_start, escaped_end, true + else + local text = template_str:sub(pos) + if text and #text > 0 then + table.insert(output, text) + end + break + end + + local text = template_str:sub(pos, next_pos - 1) + if text and #text > 0 then + table.insert(output, text) + end + + local value = values[value_index] + local str = tostring(value or "") + if escaped then + str = html_special_chars(str) + end + table.insert(output, str) + + pos = placeholder_end + 1 + value_index = value_index + 1 + end + + return table.concat(output) +end diff --git a/runner/lua/sandbox.lua b/runner/lua/sandbox.lua index dfd61d6..5e9d3d2 100644 --- a/runner/lua/sandbox.lua +++ b/runner/lua/sandbox.lua @@ -34,559 +34,3 @@ end function exit() coroutine.yield("__EXIT__") end - --- ====================================================================== --- HTTP RESPONSE FUNCTIONS --- ====================================================================== - -function http_set_status(code) - __response.status = code -end - -function http_set_header(name, value) - __response.headers = __response.headers or {} - __response.headers[name] = value -end - -function http_set_content_type(ct) - __response.headers = __response.headers or {} - __response.headers["Content-Type"] = ct -end - -function http_set_metadata(key, value) - __response.metadata = __response.metadata or {} - __response.metadata[key] = value -end - -function http_redirect(url, status) - __response.status = status or 302 - __response.headers = __response.headers or {} - __response.headers["Location"] = url - coroutine.yield("__EXIT__") -end - --- ====================================================================== --- COOKIE FUNCTIONS --- ====================================================================== - -function cookie_set(name, value, options) - __response.cookies = __response.cookies or {} - local opts = options or {} - local cookie = { - name = name, - value = value or "", - path = opts.path or "/", - domain = opts.domain, - secure = opts.secure ~= false, - http_only = opts.http_only ~= false - } - if opts.expires and opts.expires > 0 then - cookie.max_age = opts.expires - end - table.insert(__response.cookies, cookie) -end - -function cookie_get(name) - return __ctx._request_cookies and __ctx._request_cookies[name] -end - -function cookie_delete(name, path, domain) - return cookie_set(name, "", {expires = -1, path = path or "/", domain = domain}) -end - --- ====================================================================== --- SESSION FUNCTIONS --- ====================================================================== - -function session_set(key, value) - __response.session = __response.session or {} - __response.session[key] = value - if __ctx.session and __ctx.session.data then - __ctx.session.data[key] = value - end -end - -function session_get(key) - return __ctx.session and __ctx.session.data and __ctx.session.data[key] -end - -function session_id() - return __ctx.session and __ctx.session.id -end - -function session_get_all() - if __ctx.session and __ctx.session.data then - local copy = {} - for k, v in pairs(__ctx.session.data) do - copy[k] = v - end - return copy - end - return {} -end - -function session_delete(key) - __response.session = __response.session or {} - __response.session[key] = "__DELETE__" - if __ctx.session and __ctx.session.data then - __ctx.session.data[key] = nil - end -end - -function session_clear() - __response.session = {__clear_all = true} - if __ctx.session and __ctx.session.data then - for k in pairs(__ctx.session.data) do - __ctx.session.data[k] = nil - end - end -end - --- ====================================================================== --- CSRF FUNCTIONS --- ====================================================================== - -function csrf_generate() - local token = generate_token(32) - session_set("_csrf_token", token) - return token -end - -function csrf_field() - local token = session_get("_csrf_token") - if not token then - token = csrf_generate() - end - return string.format('', - html_special_chars(token)) -end - -function csrf_validate() - local token = __ctx.session and __ctx.session.data and __ctx.session.data["_csrf_token"] - if not token then - __response.status = 403 - coroutine.yield("__EXIT__") - end - - local request_token = (__ctx._request_form and __ctx._request_form._csrf_token) or - (__ctx._request_headers and (__ctx._request_headers["x-csrf-token"] or __ctx._request_headers["csrf-token"])) - - if not request_token or request_token ~= token then - __response.status = 403 - coroutine.yield("__EXIT__") - end - return true -end - --- ====================================================================== --- CONTENT TYPE HELPERS --- ====================================================================== - -function send_html(content) - http_set_content_type("text/html") - return content -end - -function send_json(content) - http_set_content_type("application/json") - return content -end - -function send_text(content) - http_set_content_type("text/plain") - return content -end - -function send_xml(content) - http_set_content_type("application/xml") - return content -end - -function send_javascript(content) - http_set_content_type("application/javascript") - return content -end - -function send_css(content) - http_set_content_type("text/css") - return content -end - -function send_svg(content) - http_set_content_type("image/svg+xml") - return content -end - -function send_csv(content) - http_set_content_type("text/csv") - return content -end - -function send_binary(content, mime_type) - http_set_content_type(mime_type or "application/octet-stream") - return content -end - --- ====================================================================== --- TEMPLATE RENDER FUNCTIONS --- ====================================================================== - --- Template processing with code execution -function render(template_str, env) - local function is_control_structure(code) - -- Check if code is a control structure that doesn't produce output - local trimmed = code:match("^%s*(.-)%s*$") - return trimmed == "else" or - trimmed == "end" or - trimmed:match("^if%s") or - trimmed:match("^elseif%s") or - trimmed:match("^for%s") or - trimmed:match("^while%s") or - trimmed:match("^repeat%s*$") or - trimmed:match("^until%s") or - trimmed:match("^do%s*$") or - trimmed:match("^local%s") or - trimmed:match("^function%s") or - trimmed:match(".*=%s*function%s*%(") or - trimmed:match(".*then%s*$") or - trimmed:match(".*do%s*$") - end - - local pos, chunks = 1, {} - while pos <= #template_str do - local unescaped_start = template_str:find("{{{", pos, true) - local escaped_start = template_str:find("{{", pos, true) - - local start, tag_type, open_len - if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then - start, tag_type, open_len = unescaped_start, "-", 3 - elseif escaped_start then - start, tag_type, open_len = escaped_start, "=", 2 - else - table.insert(chunks, template_str:sub(pos)) - break - end - - if start > pos then - table.insert(chunks, template_str:sub(pos, start-1)) - end - - pos = start + open_len - local close_tag = tag_type == "-" and "}}}" or "}}" - local close_start, close_stop = template_str:find(close_tag, pos, true) - if not close_start then - error("Failed to find closing tag at position " .. pos) - end - - local code = template_str:sub(pos, close_start-1):match("^%s*(.-)%s*$") - local is_control = is_control_structure(code) - - table.insert(chunks, {tag_type, code, pos, is_control}) - pos = close_stop + 1 - end - - local buffer = {"local _tostring, _escape, _b, _b_i = ...\n"} - for _, chunk in ipairs(chunks) do - local t = type(chunk) - if t == "string" then - table.insert(buffer, "_b_i = _b_i + 1\n") - table.insert(buffer, "_b[_b_i] = " .. string.format("%q", chunk) .. "\n") - else - local tag_type, code, pos, is_control = chunk[1], chunk[2], chunk[3], chunk[4] - - if is_control then - -- Control structure - just insert as raw Lua code - table.insert(buffer, "--[[" .. pos .. "]] " .. code .. "\n") - elseif tag_type == "=" then - -- Simple variable check - if code:match("^[%w_]+$") then - table.insert(buffer, "_b_i = _b_i + 1\n") - table.insert(buffer, "--[[" .. pos .. "]] _b[_b_i] = _escape(_tostring(" .. code .. "))\n") - else - -- Expression output with escaping - table.insert(buffer, "_b_i = _b_i + 1\n") - table.insert(buffer, "--[[" .. pos .. "]] _b[_b_i] = _escape(_tostring(" .. code .. "))\n") - end - elseif tag_type == "-" then - -- Unescaped output - table.insert(buffer, "_b_i = _b_i + 1\n") - table.insert(buffer, "--[[" .. pos .. "]] _b[_b_i] = _tostring(" .. code .. ")\n") - end - end - end - table.insert(buffer, "return _b") - - local generated_code = table.concat(buffer) - - -- DEBUG: Uncomment to see generated code - -- print("Generated Lua code:") - -- print(generated_code) - -- print("---") - - local fn, err = loadstring(generated_code) - if not fn then - print("Generated code that failed to compile:") - print(generated_code) - error(err) - end - - env = env or {} - local runtime_env = setmetatable({}, {__index = function(_, k) return env[k] or _G[k] end}) - setfenv(fn, runtime_env) - - local output_buffer = {} - fn(tostring, html_special_chars, output_buffer, 0) - return table.concat(output_buffer) -end - --- Named placeholder processing -function parse(template_str, env) - local pos, output = 1, {} - env = env or {} - - while pos <= #template_str do - local unescaped_start, unescaped_end, unescaped_name = template_str:find("{{{%s*([%w_]+)%s*}}}", pos) - local escaped_start, escaped_end, escaped_name = template_str:find("{{%s*([%w_]+)%s*}}", pos) - - local next_pos, placeholder_end, name, escaped - if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then - next_pos, placeholder_end, name, escaped = unescaped_start, unescaped_end, unescaped_name, false - elseif escaped_start then - next_pos, placeholder_end, name, escaped = escaped_start, escaped_end, escaped_name, true - else - local text = template_str:sub(pos) - if text and #text > 0 then - table.insert(output, text) - end - break - end - - local text = template_str:sub(pos, next_pos - 1) - if text and #text > 0 then - table.insert(output, text) - end - - local value = env[name] - local str = tostring(value or "") - if escaped then - str = html_special_chars(str) - end - table.insert(output, str) - - pos = placeholder_end + 1 - end - - return table.concat(output) -end - --- Indexed placeholder processing -function iparse(template_str, values) - local pos, output, value_index = 1, {}, 1 - values = values or {} - - while pos <= #template_str do - local unescaped_start, unescaped_end = template_str:find("{{{}}}", pos, true) - local escaped_start, escaped_end = template_str:find("{{}}", pos, true) - - local next_pos, placeholder_end, escaped - if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then - next_pos, placeholder_end, escaped = unescaped_start, unescaped_end, false - elseif escaped_start then - next_pos, placeholder_end, escaped = escaped_start, escaped_end, true - else - local text = template_str:sub(pos) - if text and #text > 0 then - table.insert(output, text) - end - break - end - - local text = template_str:sub(pos, next_pos - 1) - if text and #text > 0 then - table.insert(output, text) - end - - local value = values[value_index] - local str = tostring(value or "") - if escaped then - str = html_special_chars(str) - end - table.insert(output, str) - - pos = placeholder_end + 1 - value_index = value_index + 1 - end - - return table.concat(output) -end - --- ====================================================================== --- PASSWORD FUNCTIONS --- ====================================================================== - --- Hash a password using Argon2id --- Options: --- memory: Amount of memory to use in KB (default: 128MB) --- iterations: Number of iterations (default: 4) --- parallelism: Number of threads (default: 4) --- salt_length: Length of salt in bytes (default: 16) --- key_length: Length of the derived key in bytes (default: 32) -function password_hash(plain_password, options) - if type(plain_password) ~= "string" then - error("password_hash: expected string password", 2) - end - - return __password_hash(plain_password, options) -end - --- Verify a password against a hash -function password_verify(plain_password, hash_string) - if type(plain_password) ~= "string" then - error("password_verify: expected string password", 2) - end - - if type(hash_string) ~= "string" then - error("password_verify: expected string hash", 2) - end - - return __password_verify(plain_password, hash_string) -end - --- ====================================================================== --- SESSION FLASH FUNCTIONS --- ====================================================================== - -function session_flash(key, value) - __response.flash = __response.flash or {} - __response.flash[key] = value -end - -function session_get_flash(key) - -- Check current flash data first - if __response.flash and __response.flash[key] ~= nil then - return __response.flash[key] - end - - -- Check session flash data - if __ctx.session and __ctx.session.flash and __ctx.session.flash[key] ~= nil then - return __ctx.session.flash[key] - end - - return nil -end - -function session_has_flash(key) - -- Check current flash - if __response.flash and __response.flash[key] ~= nil then - return true - end - - -- Check session flash - if __ctx.session and __ctx.session.flash and __ctx.session.flash[key] ~= nil then - return true - end - - return false -end - -function session_get_all_flash() - local flash = {} - - -- Add session flash data first - if __ctx.session and __ctx.session.flash then - for k, v in pairs(__ctx.session.flash) do - flash[k] = v - end - end - - -- Add current response flash (overwrites session flash if same key) - if __response.flash then - for k, v in pairs(__response.flash) do - flash[k] = v - end - end - - return flash -end - -function session_flash_now(key, value) - -- Flash for current request only (not persisted) - _G._current_flash = _G._current_flash or {} - _G._current_flash[key] = value -end - -function session_get_flash_now(key) - return _G._current_flash and _G._current_flash[key] -end - --- ====================================================================== --- FLASH HELPER FUNCTIONS --- ====================================================================== - -function flash_success(message) - session_flash("success", message) -end - -function flash_error(message) - session_flash("error", message) -end - -function flash_warning(message) - session_flash("warning", message) -end - -function flash_info(message) - session_flash("info", message) -end - -function flash_message(type, message) - session_flash(type, message) -end - --- Get flash messages by type -function get_flash_success() - return session_get_flash("success") -end - -function get_flash_error() - return session_get_flash("error") -end - -function get_flash_warning() - return session_get_flash("warning") -end - -function get_flash_info() - return session_get_flash("info") -end - --- Check if flash messages exist -function has_flash_success() - return session_has_flash("success") -end - -function has_flash_error() - return session_has_flash("error") -end - -function has_flash_warning() - return session_has_flash("warning") -end - -function has_flash_info() - return session_has_flash("info") -end - --- Convenience function for redirects with flash -function redirect_with_flash(url, type, message, status) - session_flash(type or "info", message) - http_redirect(url, status) -end - -function redirect_with_success(url, message, status) - redirect_with_flash(url, "success", message, status) -end - -function redirect_with_error(url, message, status) - redirect_with_flash(url, "error", message, status) -end diff --git a/runner/lua/session.lua b/runner/lua/session.lua new file mode 100644 index 0000000..837d689 --- /dev/null +++ b/runner/lua/session.lua @@ -0,0 +1,179 @@ +-- session.lua + +function session_set(key, value) + __response.session = __response.session or {} + __response.session[key] = value + if __ctx.session and __ctx.session.data then + __ctx.session.data[key] = value + end +end + +function session_get(key) + return __ctx.session and __ctx.session.data and __ctx.session.data[key] +end + +function session_id() + return __ctx.session and __ctx.session.id +end + +function session_get_all() + if __ctx.session and __ctx.session.data then + local copy = {} + for k, v in pairs(__ctx.session.data) do + copy[k] = v + end + return copy + end + return {} +end + +function session_delete(key) + __response.session = __response.session or {} + __response.session[key] = "__DELETE__" + if __ctx.session and __ctx.session.data then + __ctx.session.data[key] = nil + end +end + +function session_clear() + __response.session = {__clear_all = true} + if __ctx.session and __ctx.session.data then + for k in pairs(__ctx.session.data) do + __ctx.session.data[k] = nil + end + end +end + +function session_flash(key, value) + __response.flash = __response.flash or {} + __response.flash[key] = value +end + +function session_get_flash(key) + -- Check current flash data first + if __response.flash and __response.flash[key] ~= nil then + return __response.flash[key] + end + + -- Check session flash data + if __ctx.session and __ctx.session.flash and __ctx.session.flash[key] ~= nil then + return __ctx.session.flash[key] + end + + return nil +end + +function session_has_flash(key) + -- Check current flash + if __response.flash and __response.flash[key] ~= nil then + return true + end + + -- Check session flash + if __ctx.session and __ctx.session.flash and __ctx.session.flash[key] ~= nil then + return true + end + + return false +end + +function session_get_all_flash() + local flash = {} + + -- Add session flash data first + if __ctx.session and __ctx.session.flash then + for k, v in pairs(__ctx.session.flash) do + flash[k] = v + end + end + + -- Add current response flash (overwrites session flash if same key) + if __response.flash then + for k, v in pairs(__response.flash) do + flash[k] = v + end + end + + return flash +end + +function session_flash_now(key, value) + -- Flash for current request only (not persisted) + _G._current_flash = _G._current_flash or {} + _G._current_flash[key] = value +end + +function session_get_flash_now(key) + return _G._current_flash and _G._current_flash[key] +end + +-- ====================================================================== +-- FLASH HELPER FUNCTIONS +-- ====================================================================== + +function flash_success(message) + session_flash("success", message) +end + +function flash_error(message) + session_flash("error", message) +end + +function flash_warning(message) + session_flash("warning", message) +end + +function flash_info(message) + session_flash("info", message) +end + +function flash_message(type, message) + session_flash(type, message) +end + +-- Get flash messages by type +function get_flash_success() + return session_get_flash("success") +end + +function get_flash_error() + return session_get_flash("error") +end + +function get_flash_warning() + return session_get_flash("warning") +end + +function get_flash_info() + return session_get_flash("info") +end + +-- Check if flash messages exist +function has_flash_success() + return session_has_flash("success") +end + +function has_flash_error() + return session_has_flash("error") +end + +function has_flash_warning() + return session_has_flash("warning") +end + +function has_flash_info() + return session_has_flash("info") +end + +function redirect_with_flash(url, type, message, status) + session_flash(type or "info", message) + http_redirect(url, status) +end + +function redirect_with_success(url, message, status) + redirect_with_flash(url, "success", message, status) +end + +function redirect_with_error(url, message, status) + redirect_with_flash(url, "error", message, status) +end diff --git a/runner/lua/sqlite.lua b/runner/lua/sqlite.lua index 5b042dd..e859109 100644 --- a/runner/lua/sqlite.lua +++ b/runner/lua/sqlite.lua @@ -1,3 +1,5 @@ +-- sqlite.lua + local function normalize_params(params, ...) if type(params) == "table" then return params end local args = {...} diff --git a/runner/lua/string.lua b/runner/lua/string.lua index 954fae3..5289ebc 100644 --- a/runner/lua/string.lua +++ b/runner/lua/string.lua @@ -1,6 +1,4 @@ ---[[ -string.lua - Extended string library functions -]]-- +-- string.lua local string_ext = {} @@ -17,7 +15,7 @@ end -- Split string by delimiter function string_ext.split(s, delimiter) if type(s) ~= "string" then return {} end - + delimiter = delimiter or "," local result = {} for match in (s..delimiter):gmatch("(.-)"..delimiter) do @@ -41,39 +39,39 @@ end -- Left pad a string function string_ext.pad_left(s, len, char) if type(s) ~= "string" or type(len) ~= "number" then return s end - + char = char or " " if #s >= len then return s end - + return string.rep(char:sub(1,1), len - #s) .. s end -- Right pad a string function string_ext.pad_right(s, len, char) if type(s) ~= "string" or type(len) ~= "number" then return s end - + char = char or " " if #s >= len then return s end - + return s .. string.rep(char:sub(1,1), len - #s) end -- Center a string function string_ext.center(s, width, char) if type(s) ~= "string" or width <= #s then return s end - + char = char or " " local pad_len = width - #s local left_pad = math.floor(pad_len / 2) local right_pad = pad_len - left_pad - + return string.rep(char:sub(1,1), left_pad) .. s .. string.rep(char:sub(1,1), right_pad) end -- Count occurrences of substring function string_ext.count(s, substr) if type(s) ~= "string" or type(substr) ~= "string" or #substr == 0 then return 0 end - + local count, pos = 0, 1 while true do pos = s:find(substr, pos, true) @@ -93,7 +91,7 @@ end -- Capitalize all words function string_ext.title(s) if type(s) ~= "string" then return s end - + return s:gsub("(%w)([%w]*)", function(first, rest) return first:upper() .. rest:lower() end) @@ -102,7 +100,7 @@ end -- Insert string at position function string_ext.insert(s, pos, insert_str) if type(s) ~= "string" or type(insert_str) ~= "string" then return s end - + pos = math.max(1, math.min(pos, #s + 1)) return s:sub(1, pos - 1) .. insert_str .. s:sub(pos) end @@ -110,20 +108,20 @@ end -- Remove substring function string_ext.remove(s, start, length) if type(s) ~= "string" then return s end - + length = length or 1 if start < 1 or start > #s then return s end - + return s:sub(1, start - 1) .. s:sub(start + length) end -- Replace substring once function string_ext.replace(s, old, new, n) if type(s) ~= "string" or type(old) ~= "string" or #old == 0 then return s end - + new = new or "" n = n or 1 - + return s:gsub(old:gsub("[%-%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%1"), new, n) end @@ -142,47 +140,47 @@ end -- Wrap text at specified width function string_ext.wrap(s, width, indent_first, indent_rest) if type(s) ~= "string" or type(width) ~= "number" then return s end - + width = math.max(1, width) indent_first = indent_first or "" indent_rest = indent_rest or indent_first - + local result = {} local line_prefix = indent_first local pos = 1 - + while pos <= #s do local line_width = width - #line_prefix local end_pos = math.min(pos + line_width - 1, #s) - + if end_pos < #s then local last_space = s:sub(pos, end_pos):match(".*%s()") if last_space then end_pos = pos + last_space - 2 end end - + table.insert(result, line_prefix .. s:sub(pos, end_pos)) pos = end_pos + 1 - + -- Skip leading spaces on next line while s:sub(pos, pos) == " " do pos = pos + 1 end - + line_prefix = indent_rest end - + return table.concat(result, "\n") end -- Limit string length with ellipsis function string_ext.truncate(s, length, ellipsis) if type(s) ~= "string" then return s end - + ellipsis = ellipsis or "..." if #s <= length then return s end - + return s:sub(1, length - #ellipsis) .. ellipsis end @@ -194,4 +192,4 @@ for name, func in pairs(string) do string_ext[name] = func end -return string_ext \ No newline at end of file +return string_ext diff --git a/runner/lua/table.lua b/runner/lua/table.lua index 6001909..6bb5276 100644 --- a/runner/lua/table.lua +++ b/runner/lua/table.lua @@ -1,6 +1,4 @@ ---[[ -table.lua - Extended table library functions -]]-- +-- table.lua local table_ext = {} @@ -11,66 +9,66 @@ local table_ext = {} -- Remove duplicate values (like array_unique) function table_ext.unique(t) if type(t) ~= "table" then return {} end - + local seen = {} local result = {} - + for _, v in ipairs(t) do if not seen[v] then seen[v] = true table.insert(result, v) end end - + return result end -- Return items in first table that are present in all other tables (like array_intersect) function table_ext.intersect(t1, ...) if type(t1) ~= "table" then return {} end - + local args = {...} local result = {} - + -- Convert all tables to sets for O(1) lookups local sets = {} for i, t in ipairs(args) do if type(t) ~= "table" then return {} end - + sets[i] = {} for _, v in ipairs(t) do sets[i][v] = true end end - + -- Check each element in t1 against all other tables for _, v in ipairs(t1) do local present_in_all = true - + for i = 1, #args do if not sets[i][v] then present_in_all = false break end end - + if present_in_all then table.insert(result, v) end end - + return result end -- Return items in first table that are not present in other tables (like array_diff) function table_ext.diff(t1, ...) if type(t1) ~= "table" then return {} end - + local args = {...} local result = {} - + -- Build unified set of elements from other tables local others = {} for _, t in ipairs(args) do @@ -80,14 +78,14 @@ function table_ext.diff(t1, ...) end end end - + -- Add elements from t1 that aren't in other tables for _, v in ipairs(t1) do if not others[v] then table.insert(result, v) end end - + return result end @@ -98,35 +96,35 @@ end -- Check if value exists in table (like in_array) function table_ext.contains(t, value) if type(t) ~= "table" then return false end - + for _, v in ipairs(t) do if v == value then return true end end - + return false end -- Find key for a value (like array_search) function table_ext.find(t, value) if type(t) ~= "table" then return nil end - + for k, v in pairs(t) do if v == value then return k end end - + return nil end -- Filter table elements (like array_filter) function table_ext.filter(t, func) if type(t) ~= "table" or type(func) ~= "function" then return {} end - + local result = {} - + for k, v in pairs(t) do if func(v, k) then if type(k) == "number" and k % 1 == 0 and k > 0 then @@ -138,7 +136,7 @@ function table_ext.filter(t, func) end end end - + return result end @@ -149,9 +147,9 @@ end -- Apply a function to all values (like array_map) function table_ext.map(t, func) if type(t) ~= "table" or type(func) ~= "function" then return {} end - + local result = {} - + for k, v in pairs(t) do if type(k) == "number" and k % 1 == 0 and k > 0 then -- For array-like tables, maintain numerical indices @@ -161,18 +159,18 @@ function table_ext.map(t, func) result[k] = func(v, k) end end - + return result end -- Reduce a table to a single value (like array_reduce) function table_ext.reduce(t, func, initial) - if type(t) ~= "table" or type(func) ~= "function" then + if type(t) ~= "table" or type(func) ~= "function" then return initial end - + local result = initial - + for k, v in pairs(t) do if result == nil then result = v @@ -180,7 +178,7 @@ function table_ext.reduce(t, func, initial) result = func(result, v, k) end end - + return result end @@ -190,39 +188,39 @@ end -- Split table into chunks (like array_chunk) function table_ext.chunk(t, size) - if type(t) ~= "table" or type(size) ~= "number" or size <= 0 then + if type(t) ~= "table" or type(size) ~= "number" or size <= 0 then return {} end - + local result = {} local chunk = {} local count = 0 - + for _, v in ipairs(t) do count = count + 1 chunk[count] = v - + if count == size then table.insert(result, chunk) chunk = {} count = 0 end end - + -- Add the last chunk if it has any elements if count > 0 then table.insert(result, chunk) end - + return result end -- Extract a column from a table of tables (like array_column) function table_ext.column(t, column_key, index_key) if type(t) ~= "table" or column_key == nil then return {} end - + local result = {} - + for _, row in ipairs(t) do if type(row) == "table" and row[column_key] ~= nil then if index_key ~= nil and row[index_key] ~= nil then @@ -232,14 +230,14 @@ function table_ext.column(t, column_key, index_key) end end end - + return result end -- Merge tables (like array_merge, but preserves keys) function table_ext.merge(...) local result = {} - + for _, t in ipairs({...}) do if type(t) == "table" then for k, v in pairs(t) do @@ -253,7 +251,7 @@ function table_ext.merge(...) end end end - + return result end @@ -264,41 +262,41 @@ end -- Exchange keys with values (like array_flip) function table_ext.flip(t) if type(t) ~= "table" then return {} end - + local result = {} - + for k, v in pairs(t) do if type(v) == "string" or type(v) == "number" then result[v] = k end end - + return result end -- Get all keys from a table (like array_keys) function table_ext.keys(t) if type(t) ~= "table" then return {} end - + local result = {} - + for k, _ in pairs(t) do table.insert(result, k) end - + return result end -- Get all values from a table (like array_values) function table_ext.values(t) if type(t) ~= "table" then return {} end - + local result = {} - + for _, v in pairs(t) do table.insert(result, v) end - + return result end @@ -309,47 +307,47 @@ end -- Sum all values (like array_sum) function table_ext.sum(t) if type(t) ~= "table" then return 0 end - + local sum = 0 - + for _, v in pairs(t) do if type(v) == "number" then sum = sum + v end end - + return sum end -- Multiply all values (like array_product) function table_ext.product(t) if type(t) ~= "table" then return 0 end - + local product = 1 local has_number = false - + for _, v in pairs(t) do if type(v) == "number" then product = product * v has_number = true end end - + return has_number and product or 0 end -- Count value occurrences (like array_count_values) function table_ext.count_values(t) if type(t) ~= "table" then return {} end - + local result = {} - + for _, v in pairs(t) do if type(v) == "string" or type(v) == "number" then result[v] = (result[v] or 0) + 1 end end - + return result end @@ -360,41 +358,41 @@ end -- Create a table with a range of values (like range) function table_ext.range(start, stop, step) if type(start) ~= "number" then return {} end - + step = step or 1 - + local result = {} - + if not stop then stop = start start = 1 end - + if (step > 0 and start > stop) or (step < 0 and start < stop) then return {} end - + local i = start while (step > 0 and i <= stop) or (step < 0 and i >= stop) do table.insert(result, i) i = i + step end - + return result end -- Fill a table with a value (like array_fill) function table_ext.fill(start_index, count, value) - if type(start_index) ~= "number" or type(count) ~= "number" or count < 0 then + if type(start_index) ~= "number" or type(count) ~= "number" or count < 0 then return {} end - + local result = {} - + for i = 0, count - 1 do result[start_index + i] = value end - + return result end @@ -405,70 +403,70 @@ end -- Reverse a table (array part only) function table_ext.reverse(t) if type(t) ~= "table" then return {} end - + local result = {} local count = #t - + for i = count, 1, -1 do table.insert(result, t[i]) end - + return result end -- Get the max value in a table function table_ext.max(t) if type(t) ~= "table" or #t == 0 then return nil end - + local max = t[1] - + for i = 2, #t do if t[i] > max then max = t[i] end end - + return max end -- Get the min value in a table function table_ext.min(t) if type(t) ~= "table" or #t == 0 then return nil end - + local min = t[1] - + for i = 2, #t do if t[i] < min then min = t[i] end end - + return min end -- Check if all elements satisfy a condition function table_ext.all(t, func) if type(t) ~= "table" or type(func) ~= "function" then return false end - + for k, v in pairs(t) do if not func(v, k) then return false end end - + return true end -- Check if any element satisfies a condition function table_ext.any(t, func) if type(t) ~= "table" or type(func) ~= "function" then return false end - + for k, v in pairs(t) do if func(v, k) then return true end end - + return false end @@ -485,36 +483,36 @@ end -- Get table length (works for both array and hash parts) function table_ext.size(t) if type(t) ~= "table" then return 0 end - + local count = 0 for _ in pairs(t) do count = count + 1 end - + return count end -- Get a slice of a table function table_ext.slice(t, start, stop) if type(t) ~= "table" then return {} end - + local len = #t start = start or 1 stop = stop or len - + -- Convert negative indices if start < 0 then start = len + start + 1 end if stop < 0 then stop = len + stop + 1 end - + -- Ensure bounds start = math.max(1, math.min(start, len + 1)) stop = math.max(0, math.min(stop, len)) - + local result = {} for i = start, stop do table.insert(result, t[i]) end - + return result end @@ -539,83 +537,83 @@ end -- Sort and maintain index association (like asort) function table_ext.asort(t) if type(t) ~= "table" then return t end - + local keys, result = {}, {} for k in pairs(t) do table.insert(keys, k) end - - table.sort(keys, function(a, b) + + table.sort(keys, function(a, b) return t[a] < t[b] end) - + for _, k in ipairs(keys) do result[k] = t[k] end - + return result end -- Sort in reverse order and maintain index association (like arsort) function table_ext.arsort(t) if type(t) ~= "table" then return t end - + local keys, result = {}, {} for k in pairs(t) do table.insert(keys, k) end - - table.sort(keys, function(a, b) + + table.sort(keys, function(a, b) return t[a] > t[b] end) - + for _, k in ipairs(keys) do result[k] = t[k] end - + return result end -- Sort by keys (like ksort) function table_ext.ksort(t) if type(t) ~= "table" then return t end - + local keys, result = {}, {} for k in pairs(t) do table.insert(keys, k) end - + table.sort(keys) - + for _, k in ipairs(keys) do result[k] = t[k] end - + return result end -- Sort by keys in reverse order (like krsort) function table_ext.krsort(t) if type(t) ~= "table" then return t end - + local keys, result = {}, {} for k in pairs(t) do table.insert(keys, k) end - + table.sort(keys, function(a, b) return a > b end) - + for _, k in ipairs(keys) do result[k] = t[k] end - + return result end -- Sort using custom comparison function (like usort) function table_ext.usort(t, compare_func) if type(t) ~= "table" or type(compare_func) ~= "function" then return t end - + table.sort(t, compare_func) return t end @@ -623,45 +621,45 @@ end -- Sort maintaining keys using custom comparison function (like uasort) function table_ext.uasort(t, compare_func) if type(t) ~= "table" or type(compare_func) ~= "function" then return t end - + local keys, result = {}, {} for k in pairs(t) do table.insert(keys, k) end - - table.sort(keys, function(a, b) + + table.sort(keys, function(a, b) return compare_func(t[a], t[b]) end) - + for _, k in ipairs(keys) do result[k] = t[k] end - + return result end -- Sort by keys using custom comparison function (like uksort) function table_ext.uksort(t, compare_func) if type(t) ~= "table" or type(compare_func) ~= "function" then return t end - + local keys, result = {}, {} for k in pairs(t) do table.insert(keys, k) end - + table.sort(keys, compare_func) - + for _, k in ipairs(keys) do result[k] = t[k] end - + return result end -- Natural order sort (like natsort) function table_ext.natsort(t) if type(t) ~= "table" then return t end - + local function natural_compare(a, b) local function get_chunks(s) if type(s) ~= "string" then s = tostring(s) end @@ -674,10 +672,10 @@ function table_ext.natsort(t) end return chunks end - + local a_chunks = get_chunks(a) local b_chunks = get_chunks(b) - + for i = 1, math.min(#a_chunks, #b_chunks) do if a_chunks[i].n ~= b_chunks[i].n then return a_chunks[i].n -- numbers come before strings @@ -689,10 +687,10 @@ function table_ext.natsort(t) end end end - + return #a_chunks < #b_chunks end - + table.sort(t, natural_compare) return t end @@ -700,7 +698,7 @@ end -- Natural case-insensitive sort (like natcasesort) function table_ext.natcasesort(t) if type(t) ~= "table" then return t end - + local function case_insensitive_natural_compare(a, b) if type(a) == "string" and type(b) == "string" then return table_ext.natural_compare(a:lower(), b:lower()) @@ -708,7 +706,7 @@ function table_ext.natcasesort(t) return table_ext.natural_compare(a, b) end end - + return table_ext.usort(t, case_insensitive_natural_compare) end @@ -719,20 +717,20 @@ end -- Push one or more elements onto the end (like array_push) function table_ext.push(t, ...) if type(t) ~= "table" then return 0 end - + local count = 0 for _, v in ipairs({...}) do table.insert(t, v) count = count + 1 end - + return count end -- Pop the element off the end (like array_pop) function table_ext.pop(t) if type(t) ~= "table" or #t == 0 then return nil end - + local value = t[#t] t[#t] = nil return value @@ -741,7 +739,7 @@ end -- Shift an element off the beginning (like array_shift) function table_ext.shift(t) if type(t) ~= "table" or #t == 0 then return nil end - + local value = t[1] table.remove(t, 1) return value @@ -750,24 +748,24 @@ end -- Prepend elements to the beginning (like array_unshift) function table_ext.unshift(t, ...) if type(t) ~= "table" then return 0 end - + local args = {...} for i = #args, 1, -1 do table.insert(t, 1, args[i]) end - + return #t end -- Pad array to specified length (like array_pad) function table_ext.pad(t, size, value) if type(t) ~= "table" then return {} end - + local result = table_ext.deep_copy(t) local current_size = #result - + if size == current_size then return result end - + if size > current_size then -- Pad to the right for i = current_size + 1, size do @@ -794,32 +792,32 @@ function table_ext.pad(t, size, value) end end end - + return result end -- Remove a portion and replace it (like array_splice) function table_ext.splice(t, offset, length, ...) if type(t) ~= "table" then return {} end - + local result = table_ext.deep_copy(t) local size = #result - + -- Handle negative offset if offset < 0 then offset = size + offset end - + -- Ensure offset is valid offset = math.max(1, math.min(offset, size + 1)) - + -- Handle negative or nil length if length == nil then length = size - offset + 1 elseif length < 0 then length = math.max(0, size - offset + length + 1) end - + -- Extract removed portion local removed = {} for i = offset, offset + length - 1 do @@ -827,66 +825,66 @@ function table_ext.splice(t, offset, length, ...) table.insert(removed, result[i]) end end - + -- Remove portion from original for i = 1, length do table.remove(result, offset) end - + -- Insert replacement values local replacements = {...} for i = #replacements, 1, -1 do table.insert(result, offset, replacements[i]) end - + return removed, result end -- Randomize array order (like shuffle) function table_ext.shuffle(t) if type(t) ~= "table" then return t end - + local result = table_ext.deep_copy(t) local size = #result - + for i = size, 2, -1 do local j = math.random(i) result[i], result[j] = result[j], result[i] end - + return result end -- Pick random keys from array (like array_rand) function table_ext.rand(t, num_keys) if type(t) ~= "table" then return nil end - + local size = #t if size == 0 then return nil end - + num_keys = num_keys or 1 num_keys = math.min(num_keys, size) - + if num_keys <= 0 then return nil end - + if num_keys == 1 then return math.random(size) else local keys = {} local result = {} - + -- Create a list of all possible keys for i = 1, size do keys[i] = i end - + -- Select random keys for i = 1, num_keys do local j = math.random(#keys) table.insert(result, keys[j]) table.remove(keys, j) end - + table.sort(result) return result end @@ -905,40 +903,40 @@ end -- Get the first key (like array_key_first) function table_ext.key_first(t) if type(t) ~= "table" then return nil end - + -- For array-like tables if #t > 0 then return 1 end - + -- For associative tables local first_key = nil for k in pairs(t) do first_key = k break end - + return first_key end -- Get the last key (like array_key_last) function table_ext.key_last(t) if type(t) ~= "table" then return nil end - + -- For array-like tables if #t > 0 then return #t end - + -- For associative tables (no guaranteed order, return any key) local last_key = nil for k in pairs(t) do last_key = k end - + return last_key end -- Check if table is a list (like array_is_list) function table_ext.is_list(t) if type(t) ~= "table" then return false end - + local count = 0 for k in pairs(t) do count = count + 1 @@ -946,7 +944,7 @@ function table_ext.is_list(t) return false end end - + return true end @@ -957,24 +955,24 @@ end -- Create array with keys from one array, values from another (like array_combine) function table_ext.combine(keys, values) if type(keys) ~= "table" or type(values) ~= "table" then return {} end - + local result = {} local key_count = #keys local value_count = #values - + for i = 1, math.min(key_count, value_count) do result[keys[i]] = values[i] end - + return result end -- Replace elements from one array into another (like array_replace) function table_ext.replace(t, ...) if type(t) ~= "table" then return {} end - + local result = table_ext.deep_copy(t) - + for _, replacement in ipairs({...}) do if type(replacement) == "table" then for k, v in pairs(replacement) do @@ -982,16 +980,16 @@ function table_ext.replace(t, ...) end end end - + return result end -- Replace elements recursively (like array_replace_recursive) function table_ext.replace_recursive(t, ...) if type(t) ~= "table" then return {} end - + local result = table_ext.deep_copy(t) - + for _, replacement in ipairs({...}) do if type(replacement) == "table" then for k, v in pairs(replacement) do @@ -1003,25 +1001,25 @@ function table_ext.replace_recursive(t, ...) end end end - + return result end -- Apply function to each element (like array_walk) function table_ext.walk(t, callback, user_data) if type(t) ~= "table" or type(callback) ~= "function" then return t end - + for k, v in pairs(t) do t[k] = callback(v, k, user_data) end - + return t end -- Apply function recursively (like array_walk_recursive) function table_ext.walk_recursive(t, callback, user_data) if type(t) ~= "table" or type(callback) ~= "function" then return t end - + for k, v in pairs(t) do if type(v) == "table" then table_ext.walk_recursive(v, callback, user_data) @@ -1029,7 +1027,7 @@ function table_ext.walk_recursive(t, callback, user_data) t[k] = callback(v, k, user_data) end end - + return t end @@ -1037,17 +1035,17 @@ end function table_ext.multisort(...) local args = {...} if #args == 0 then return end - + -- First argument is the main table local main = args[1] if type(main) ~= "table" then return end - + -- Create a table of indices local indices = {} for i = 1, #main do indices[i] = i end - + -- Sort the indices based on the arrays table.sort(indices, function(a, b) for i = 1, #args do @@ -1060,7 +1058,7 @@ function table_ext.multisort(...) end return a < b end) - + -- Reorder all arrays based on sorted indices for i = 1, #args do local arr = args[i] @@ -1089,4 +1087,4 @@ for name, func in pairs(table) do table_ext[name] = func end -return table_ext \ No newline at end of file +return table_ext diff --git a/runner/lua/time.lua b/runner/lua/time.lua index 5026b45..2e37a86 100644 --- a/runner/lua/time.lua +++ b/runner/lua/time.lua @@ -1,6 +1,4 @@ ---[[ -time.lua - High performance timing functions -]]-- +-- time.lua local ffi = require('ffi') local is_windows = (ffi.os == "Windows") diff --git a/runner/lua/util.lua b/runner/lua/util.lua index a3e7871..3874ca4 100644 --- a/runner/lua/util.lua +++ b/runner/lua/util.lua @@ -1,6 +1,4 @@ ---[[ -util.lua - Utility functions for the Lua sandbox -]]-- +-- util.lua -- ====================================================================== -- CORE UTILITY FUNCTIONS @@ -20,7 +18,7 @@ function html_special_chars(str) if type(str) ~= "string" then return str end - + return __html_special_chars(str) end @@ -29,7 +27,7 @@ function html_entities(str) if type(str) ~= "string" then return str end - + return __html_entities(str) end @@ -38,13 +36,13 @@ function html_entity_decode(str) if type(str) ~= "string" then return str end - + str = str:gsub("<", "<") str = str:gsub(">", ">") str = str:gsub(""", '"') str = str:gsub("'", "'") str = str:gsub("&", "&") - + return str end @@ -53,7 +51,7 @@ function nl2br(str) if type(str) ~= "string" then return str end - + return str:gsub("\r\n", "
"):gsub("\n", "
"):gsub("\r", "
") end @@ -66,7 +64,7 @@ function url_encode(str) if type(str) ~= "string" then return str end - + str = str:gsub("\n", "\r\n") str = str:gsub("([^%w %-%_%.%~])", function(c) return string.format("%%%02X", string.byte(c)) @@ -80,7 +78,7 @@ function url_decode(str) if type(str) ~= "string" then return str end - + str = str:gsub("+", " ") str = str:gsub("%%(%x%x)", function(h) return string.char(tonumber(h, 16)) @@ -97,7 +95,7 @@ function is_email(str) if type(str) ~= "string" then return false end - + -- Simple email validation pattern local pattern = "^[%w%.%%%+%-]+@[%w%.%%%+%-]+%.%w%w%w?%w?$" return str:match(pattern) ~= nil @@ -108,7 +106,7 @@ function is_url(str) if type(str) ~= "string" then return false end - + -- Simple URL validation local pattern = "^https?://[%w-_%.%?%.:/%+=&%%]+$" return str:match(pattern) ~= nil @@ -119,14 +117,14 @@ function is_ipv4(str) if type(str) ~= "string" then return false end - + local pattern = "^(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)$" local a, b, c, d = str:match(pattern) - + if not (a and b and c and d) then return false end - + a, b, c, d = tonumber(a), tonumber(b), tonumber(c), tonumber(d) return a <= 255 and b <= 255 and c <= 255 and d <= 255 end @@ -138,7 +136,7 @@ function is_int(str) elseif type(str) ~= "string" then return false end - + return str:match("^-?%d+$") ~= nil end @@ -149,7 +147,7 @@ function is_float(str) elseif type(str) ~= "string" then return false end - + return str:match("^-?%d+%.?%d*$") ~= nil end @@ -160,7 +158,7 @@ function is_bool(value) elseif type(value) ~= "string" and type(value) ~= "number" then return false end - + local v = type(value) == "string" and value:lower() or value return v == "1" or v == "true" or v == "on" or v == "yes" or v == "0" or v == "false" or v == "off" or v == "no" or @@ -174,7 +172,7 @@ function to_bool(value) elseif type(value) ~= "string" and type(value) ~= "number" then return false end - + local v = type(value) == "string" and value:lower() or value return v == "1" or v == "true" or v == "on" or v == "yes" or v == 1 end @@ -184,7 +182,7 @@ function sanitize_string(str) if type(str) ~= "string" then return "" end - + return html_special_chars(str) end @@ -193,7 +191,7 @@ function sanitize_int(value) if type(value) ~= "string" and type(value) ~= "number" then return 0 end - + value = tostring(value) local result = value:match("^-?%d+") return result and tonumber(result) or 0 @@ -204,7 +202,7 @@ function sanitize_float(value) if type(value) ~= "string" and type(value) ~= "number" then return 0 end - + value = tostring(value) local result = value:match("^-?%d+%.?%d*") return result and tonumber(result) or 0 @@ -215,20 +213,20 @@ function sanitize_url(str) if type(str) ~= "string" then return "" end - + -- Basic sanitization by removing control characters str = str:gsub("[\000-\031]", "") - + -- Make sure it's a valid URL if is_url(str) then return str end - + -- Try to prepend http:// if it's missing if not str:match("^https?://") and is_url("http://" .. str) then return "http://" .. str end - + return "" end @@ -237,15 +235,15 @@ function sanitize_email(str) if type(str) ~= "string" then return "" end - + -- Remove all characters except common email characters str = str:gsub("[^%a%d%!%#%$%%%&%'%*%+%-%/%=%?%^%_%`%{%|%}%~%@%.%[%]]", "") - + -- Return only if it's a valid email if is_email(str) then return str end - + return "" end @@ -258,19 +256,19 @@ function xss_clean(str) if type(str) ~= "string" then return str end - + -- Convert problematic characters to entities local result = html_special_chars(str) - + -- Remove JavaScript event handlers result = result:gsub("on%w+%s*=", "") - + -- Remove JavaScript protocol result = result:gsub("javascript:", "") - + -- Remove CSS expression result = result:gsub("expression%s*%(", "") - + return result end @@ -279,7 +277,7 @@ function base64_encode(str) if type(str) ~= "string" then return str end - + return __base64_encode(str) end @@ -288,6 +286,38 @@ function base64_decode(str) if type(str) ~= "string" then return str end - + return __base64_decode(str) -end \ No newline at end of file +end + +-- ====================================================================== +-- PASSWORD FUNCTIONS +-- ====================================================================== + +-- Hash a password using Argon2id +-- Options: +-- memory: Amount of memory to use in KB (default: 128MB) +-- iterations: Number of iterations (default: 4) +-- parallelism: Number of threads (default: 4) +-- salt_length: Length of salt in bytes (default: 16) +-- key_length: Length of the derived key in bytes (default: 32) +function password_hash(plain_password, options) + if type(plain_password) ~= "string" then + error("password_hash: expected string password", 2) + end + + return __password_hash(plain_password, options) +end + +-- Verify a password against a hash +function password_verify(plain_password, hash_string) + if type(plain_password) ~= "string" then + error("password_verify: expected string password", 2) + end + + if type(hash_string) ~= "string" then + error("password_verify: expected string hash", 2) + end + + return __password_verify(plain_password, hash_string) +end