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