diff --git a/core/runner/embed.go b/core/runner/embed.go
index 55569c3..e4d2c8b 100644
--- a/core/runner/embed.go
+++ b/core/runner/embed.go
@@ -22,119 +22,69 @@ var sqliteLuaCode string
//go:embed fs.lua
var fsLuaCode string
-// Global bytecode cache to improve performance
+//go:embed util.lua
+var utilLuaCode string
+
+//go:embed string.lua
+var stringLuaCode string
+
+// ModuleInfo holds information about an embeddable Lua module
+type ModuleInfo struct {
+ Name string // Module name
+ Code string // Module source code
+ Bytecode atomic.Pointer[[]byte] // Cached bytecode
+ Once sync.Once // For one-time compilation
+}
+
var (
- sandboxBytecode atomic.Pointer[[]byte]
- jsonBytecode atomic.Pointer[[]byte]
- sqliteBytecode atomic.Pointer[[]byte]
- fsBytecode atomic.Pointer[[]byte]
- bytecodeOnce sync.Once
- jsonBytecodeOnce sync.Once
- sqliteBytecodeOnce sync.Once
- fsBytecodeOnce sync.Once
+ sandbox = ModuleInfo{Name: "sandbox", Code: sandboxLuaCode}
+ modules = []ModuleInfo{
+ {Name: "json", Code: jsonLuaCode},
+ {Name: "sqlite", Code: sqliteLuaCode},
+ {Name: "fs", Code: fsLuaCode},
+ {Name: "util", Code: utilLuaCode},
+ {Name: "string", Code: stringLuaCode},
+ }
)
-// precompileSandboxCode compiles the sandbox.lua code to bytecode once
-func precompileSandboxCode() {
- tempState := luajit.New()
- if tempState == nil {
- logger.Fatal("Failed to create temp Lua state for bytecode compilation")
- }
- defer tempState.Close()
- defer tempState.Cleanup()
+// precompileModule compiles a module's code to bytecode once
+func precompileModule(m *ModuleInfo) {
+ m.Once.Do(func() {
+ tempState := luajit.New()
+ if tempState == nil {
+ logger.Fatal("Failed to create temp Lua state for %s module compilation", m.Name)
+ return
+ }
+ defer tempState.Close()
+ defer tempState.Cleanup()
- code, err := tempState.CompileBytecode(sandboxLuaCode, "sandbox.lua")
- if err != nil {
- logger.Error("Failed to compile sandbox code: %v", err)
- return
- }
-
- bytecode := make([]byte, len(code))
- copy(bytecode, code)
- sandboxBytecode.Store(&bytecode)
-
- logger.Debug("Successfully precompiled sandbox.lua to bytecode (%d bytes)", len(code))
-}
-
-// precompileJsonModule compiles the json.lua code to bytecode once
-func precompileJsonModule() {
- tempState := luajit.New()
- if tempState == nil {
- logger.Fatal("Failed to create temp Lua state for JSON module compilation")
- }
- defer tempState.Close()
- defer tempState.Cleanup()
-
- code, err := tempState.CompileBytecode(jsonLuaCode, "json.lua")
- if err != nil {
- logger.Error("Failed to compile JSON module: %v", err)
- return
- }
-
- bytecode := make([]byte, len(code))
- copy(bytecode, code)
- jsonBytecode.Store(&bytecode)
-
- logger.Debug("Successfully precompiled json.lua to bytecode (%d bytes)", len(code))
-}
-
-// precompileSqliteModule compiles the sqlite.lua code to bytecode once
-func precompileSqliteModule() {
- tempState := luajit.New()
- if tempState == nil {
- logger.Fatal("Failed to create temp Lua state for SQLite module compilation")
- }
- defer tempState.Close()
- defer tempState.Cleanup()
-
- code, err := tempState.CompileBytecode(sqliteLuaCode, "sqlite.lua")
- if err != nil {
- logger.Error("Failed to compile SQLite module: %v", err)
- return
- }
-
- bytecode := make([]byte, len(code))
- copy(bytecode, code)
- sqliteBytecode.Store(&bytecode)
-
- logger.Debug("Successfully precompiled sqlite.lua to bytecode (%d bytes)", len(code))
-}
-
-func precompileFsModule() {
- tempState := luajit.New()
- if tempState == nil {
- logger.Fatal("Failed to create temp Lua state for FS module compilation")
- }
- defer tempState.Close()
- defer tempState.Cleanup()
-
- code, err := tempState.CompileBytecode(fsLuaCode, "fs.lua")
- if err != nil {
- logger.Error("Failed to compile FS module: %v", err)
- return
- }
-
- bytecode := make([]byte, len(code))
- copy(bytecode, code)
- fsBytecode.Store(&bytecode)
-
- logger.Debug("Successfully precompiled fs.lua to bytecode (%d bytes)", len(code))
-}
-
-// loadSandboxIntoState loads the sandbox code into a Lua state
-func loadSandboxIntoState(state *luajit.State, verbose bool) error {
- bytecodeOnce.Do(precompileSandboxCode)
- jsonBytecodeOnce.Do(precompileJsonModule)
- sqliteBytecodeOnce.Do(precompileSqliteModule)
-
- // First load and execute the JSON module
- jsBytecode := jsonBytecode.Load()
- if jsBytecode != nil && len(*jsBytecode) > 0 {
- if verbose {
- logger.Debug("Loading json.lua from precompiled bytecode")
+ code, err := tempState.CompileBytecode(m.Code, m.Name+".lua")
+ if err != nil {
+ logger.Error("Failed to compile %s module: %v", m.Name, err)
+ return
}
- if err := state.LoadBytecode(*jsBytecode, "json.lua"); err != nil {
+ bytecode := make([]byte, len(code))
+ copy(bytecode, code)
+ m.Bytecode.Store(&bytecode)
+
+ logger.Debug("Successfully precompiled %s.lua to bytecode (%d bytes)", m.Name, len(code))
+ })
+}
+
+// loadModule loads a module into a Lua state
+func loadModule(state *luajit.State, m *ModuleInfo, verbose bool) error {
+ // Ensure bytecode is compiled
+ precompileModule(m)
+
+ // Attempt to load from bytecode
+ bytecode := m.Bytecode.Load()
+ if bytecode != nil && len(*bytecode) > 0 {
+ if verbose {
+ logger.Debug("Loading %s.lua from precompiled bytecode", m.Name)
+ }
+
+ if err := state.LoadBytecode(*bytecode, m.Name+".lua"); err != nil {
return err
}
@@ -142,75 +92,38 @@ func loadSandboxIntoState(state *luajit.State, verbose bool) error {
return err
}
- state.SetGlobal("json")
+ state.SetGlobal(m.Name)
} else {
+ // Fallback to interpreting the source
if verbose {
- logger.Warning("Using non-precompiled json.lua")
+ logger.Warning("Using non-precompiled %s.lua", m.Name)
}
- if err := state.DoString(jsonLuaCode); err != nil {
+ if err := state.DoString(m.Code); err != nil {
return err
}
}
- // Initialize active connections tracking
+ return nil
+}
+
+// loadSandboxIntoState loads all modules and sandbox into a Lua state
+func loadSandboxIntoState(state *luajit.State, verbose bool) error {
+ // Load all modules first
+ for i := range modules {
+ if err := loadModule(state, &modules[i], verbose); err != nil {
+ return err
+ }
+ }
+
+ // Initialize active connections tracking (specific to SQLite)
if err := state.DoString(`__active_sqlite_connections = {}`); err != nil {
return err
}
- // Load SQLite module
- sqlBytecode := sqliteBytecode.Load()
- if sqlBytecode != nil && len(*sqlBytecode) > 0 {
- if verbose {
- logger.Debug("Loading sqlite.lua from precompiled bytecode")
- }
-
- if err := state.LoadBytecode(*sqlBytecode, "sqlite.lua"); err != nil {
- return err
- }
-
- if err := state.RunBytecodeWithResults(1); err != nil {
- return err
- }
-
- state.SetGlobal("sqlite")
- } else {
- if verbose {
- logger.Warning("Using non-precompiled sqlite.lua")
- }
-
- if err := state.DoString(sqliteLuaCode); err != nil {
- return err
- }
- }
-
- fsBytecodeOnce.Do(precompileFsModule)
- fsBytecode := fsBytecode.Load()
- if fsBytecode != nil && len(*fsBytecode) > 0 {
- if verbose {
- logger.Debug("Loading fs.lua from precompiled bytecode")
- }
-
- if err := state.LoadBytecode(*fsBytecode, "fs.lua"); err != nil {
- return err
- }
-
- if err := state.RunBytecodeWithResults(1); err != nil {
- return err
- }
-
- state.SetGlobal("fs")
- } else {
- if verbose {
- logger.Warning("Using non-precompiled fs.lua")
- }
-
- if err := state.DoString(fsLuaCode); err != nil {
- return err
- }
- }
-
- bytecode := sandboxBytecode.Load()
+ // Load the sandbox last
+ precompileModule(&sandbox)
+ bytecode := sandbox.Bytecode.Load()
if bytecode != nil && len(*bytecode) > 0 {
if verbose {
logger.Debug("Loading sandbox.lua from precompiled bytecode")
diff --git a/core/runner/sandbox.go b/core/runner/sandbox.go
index 341bd98..bc8122c 100644
--- a/core/runner/sandbox.go
+++ b/core/runner/sandbox.go
@@ -113,6 +113,10 @@ func (s *Sandbox) registerCoreFunctions(state *luajit.State) error {
return err
}
+ if err := RegisterUtilFunctions(state); err != nil {
+ return err
+ }
+
return nil
}
diff --git a/core/runner/sandbox.lua b/core/runner/sandbox.lua
index 5eede95..3319e1e 100644
--- a/core/runner/sandbox.lua
+++ b/core/runner/sandbox.lua
@@ -1,9 +1,5 @@
--[[
-Moonshark Lua Sandbox Environment
-
-This file contains all the Lua code needed for the sandbox environment,
-including core modules and utilities. It's designed to be embedded in the
-Go binary at build time.
+sandbox.lua
]]--
__http_response = {}
@@ -421,60 +417,6 @@ local csrf = {
end
}
--- ======================================================================
--- UTIL MODULE
--- ======================================================================
-
--- Utility module implementation
-local util = {
- generate_token = function(length)
- return __generate_token(length or 32)
- end,
-
- -- Deep copy of tables
- deep_copy = function(obj)
- if type(obj) ~= 'table' then return obj end
- local res = {}
- for k, v in pairs(obj) do res[k] = util.deep_copy(v) end
- return res
- end,
-
- -- Merge tables
- merge_tables = function(t1, t2)
- if type(t1) ~= 'table' or type(t2) ~= 'table' then
- error("Both arguments must be tables", 2)
- end
-
- local result = util.deep_copy(t1)
- for k, v in pairs(t2) do
- if type(v) == 'table' and type(result[k]) == 'table' then
- result[k] = util.merge_tables(result[k], v)
- else
- result[k] = v
- end
- end
- return result
- end,
-
- -- String utilities
- string = {
- -- Trim whitespace
- trim = function(s)
- return (s:gsub("^%s*(.-)%s*$", "%1"))
- end,
-
- -- Split string
- split = function(s, delimiter)
- delimiter = delimiter or ","
- local result = {}
- for match in (s..delimiter):gmatch("(.-)"..delimiter) do
- table.insert(result, match)
- end
- return result
- end
- }
-}
-
-- ======================================================================
-- TEMPLATE RENDER FUNCTION
-- ======================================================================
@@ -619,5 +561,4 @@ _G.http = http
_G.session = session
_G.csrf = csrf
_G.cookie = cookie
-_G.util = util
_G.password = password
diff --git a/core/runner/string.lua b/core/runner/string.lua
new file mode 100644
index 0000000..c29960d
--- /dev/null
+++ b/core/runner/string.lua
@@ -0,0 +1,197 @@
+--[[
+string.lua - Extended string library functions
+]]--
+
+local string_ext = {}
+
+-- ======================================================================
+-- STRING UTILITY FUNCTIONS
+-- ======================================================================
+
+-- Trim whitespace from both ends
+function string_ext.trim(s)
+ if type(s) ~= "string" then return s end
+ return s:match("^%s*(.-)%s*$")
+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
+ table.insert(result, match)
+ end
+ return result
+end
+
+-- Check if string starts with prefix
+function string_ext.starts_with(s, prefix)
+ if type(s) ~= "string" or type(prefix) ~= "string" then return false end
+ return s:sub(1, #prefix) == prefix
+end
+
+-- Check if string ends with suffix
+function string_ext.ends_with(s, suffix)
+ if type(s) ~= "string" or type(suffix) ~= "string" then return false end
+ return suffix == "" or s:sub(-#suffix) == suffix
+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)
+ if not pos then break end
+ count = count + 1
+ pos = pos + 1
+ end
+ return count
+end
+
+-- Capitalize first letter
+function string_ext.capitalize(s)
+ if type(s) ~= "string" or #s == 0 then return s end
+ return s:sub(1,1):upper() .. s:sub(2)
+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)
+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
+
+-- 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
+
+-- Check if string contains substring
+function string_ext.contains(s, substr)
+ if type(s) ~= "string" or type(substr) ~= "string" then return false end
+ return s:find(substr, 1, true) ~= nil
+end
+
+-- Escape pattern magic characters
+function string_ext.escape_pattern(s)
+ if type(s) ~= "string" then return s end
+ return s:gsub("[%-%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%1")
+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
+
+-- ======================================================================
+-- INSTALL EXTENSIONS INTO STRING LIBRARY
+-- ======================================================================
+
+for name, func in pairs(string_ext) do
+ string[name] = func
+end
+
+return string_ext
\ No newline at end of file
diff --git a/core/runner/util.go b/core/runner/util.go
new file mode 100644
index 0000000..e8c3010
--- /dev/null
+++ b/core/runner/util.go
@@ -0,0 +1,116 @@
+package runner
+
+import (
+ "encoding/base64"
+ "html"
+ "strings"
+
+ luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
+)
+
+// RegisterUtilFunctions registers utility functions with the Lua state
+func RegisterUtilFunctions(state *luajit.State) error {
+ // HTML special chars
+ if err := state.RegisterGoFunction("__html_special_chars", htmlSpecialChars); err != nil {
+ return err
+ }
+
+ // HTML entities
+ if err := state.RegisterGoFunction("__html_entities", htmlEntities); err != nil {
+ return err
+ }
+
+ // Base64 encode
+ if err := state.RegisterGoFunction("__base64_encode", base64Encode); err != nil {
+ return err
+ }
+
+ // Base64 decode
+ if err := state.RegisterGoFunction("__base64_decode", base64Decode); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// htmlSpecialChars converts special characters to HTML entities
+func htmlSpecialChars(state *luajit.State) int {
+ if !state.IsString(1) {
+ state.PushNil()
+ return 1
+ }
+
+ input := state.ToString(1)
+ result := html.EscapeString(input)
+ state.PushString(result)
+ return 1
+}
+
+// htmlEntities is a more comprehensive version of htmlSpecialChars
+func htmlEntities(state *luajit.State) int {
+ if !state.IsString(1) {
+ state.PushNil()
+ return 1
+ }
+
+ input := state.ToString(1)
+ // First use HTML escape for standard entities
+ result := html.EscapeString(input)
+
+ // Additional entities beyond what html.EscapeString handles
+ replacements := map[string]string{
+ "©": "©",
+ "®": "®",
+ "™": "™",
+ "€": "€",
+ "£": "£",
+ "¥": "¥",
+ "—": "—",
+ "–": "–",
+ "…": "…",
+ "•": "•",
+ "°": "°",
+ "±": "±",
+ "¼": "¼",
+ "½": "½",
+ "¾": "¾",
+ }
+
+ for char, entity := range replacements {
+ result = strings.ReplaceAll(result, char, entity)
+ }
+
+ state.PushString(result)
+ return 1
+}
+
+// base64Encode encodes a string to base64
+func base64Encode(state *luajit.State) int {
+ if !state.IsString(1) {
+ state.PushNil()
+ return 1
+ }
+
+ input := state.ToString(1)
+ result := base64.StdEncoding.EncodeToString([]byte(input))
+ state.PushString(result)
+ return 1
+}
+
+// base64Decode decodes a base64 string
+func base64Decode(state *luajit.State) int {
+ if !state.IsString(1) {
+ state.PushNil()
+ return 1
+ }
+
+ input := state.ToString(1)
+ result, err := base64.StdEncoding.DecodeString(input)
+ if err != nil {
+ state.PushNil()
+ return 1
+ }
+
+ state.PushString(string(result))
+ return 1
+}
diff --git a/core/runner/util.lua b/core/runner/util.lua
new file mode 100644
index 0000000..ed4c219
--- /dev/null
+++ b/core/runner/util.lua
@@ -0,0 +1,335 @@
+--[[
+util.lua - Utility functions for the Lua sandbox
+Enhanced with web development utilities
+]]--
+
+local util = {}
+
+-- ======================================================================
+-- CORE UTILITY FUNCTIONS
+-- ======================================================================
+
+-- Generate a random token
+function util.generate_token(length)
+ return __generate_token(length or 32)
+end
+
+-- Deep copy of tables
+function util.deep_copy(obj)
+ if type(obj) ~= 'table' then return obj end
+ local res = {}
+ for k, v in pairs(obj) do res[k] = util.deep_copy(v) end
+ return res
+end
+
+-- Merge tables
+function util.merge_tables(t1, t2)
+ if type(t1) ~= 'table' or type(t2) ~= 'table' then
+ error("Both arguments must be tables", 2)
+ end
+
+ local result = util.deep_copy(t1)
+ for k, v in pairs(t2) do
+ if type(v) == 'table' and type(result[k]) == 'table' then
+ result[k] = util.merge_tables(result[k], v)
+ else
+ result[k] = v
+ end
+ end
+ return result
+end
+
+-- ======================================================================
+-- HTML ENTITY FUNCTIONS
+-- ======================================================================
+
+-- HTML entity mapping for common characters
+local html_entities = {
+ ["&"] = "&",
+ ["<"] = "<",
+ [">"] = ">",
+ ['"'] = """,
+ ["'"] = "'",
+ ["/"] = "/",
+ ["`"] = "`",
+ ["="] = "="
+}
+
+-- Convert special characters to HTML entities (like htmlspecialchars)
+function util.html_special_chars(str)
+ if type(str) ~= "string" then
+ return str
+ end
+
+ return __html_special_chars(str)
+end
+
+-- Convert all applicable characters to HTML entities (like htmlentities)
+function util.html_entities(str)
+ if type(str) ~= "string" then
+ return str
+ end
+
+ return __html_entities(str)
+end
+
+-- Convert HTML entities back to characters (simple version)
+function util.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
+
+-- Convert newlines to
tags
+function util.nl2br(str)
+ if type(str) ~= "string" then
+ return str
+ end
+
+ return str:gsub("\r\n", "
"):gsub("\n", "
"):gsub("\r", "
")
+end
+
+-- ======================================================================
+-- URL FUNCTIONS
+-- ======================================================================
+
+-- URL encode a string
+function util.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))
+ end)
+ str = str:gsub(" ", "+")
+ return str
+end
+
+-- URL decode a string
+function util.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))
+ end)
+ return str
+end
+
+-- ======================================================================
+-- VALIDATION FUNCTIONS
+-- ======================================================================
+
+-- Email validation
+function util.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
+end
+
+-- URL validation
+function util.is_url(str)
+ if type(str) ~= "string" then
+ return false
+ end
+
+ -- Simple URL validation
+ local pattern = "^https?://[%w-_%.%?%.:/%+=&%%]+$"
+ return str:match(pattern) ~= nil
+end
+
+-- IP address validation (IPv4)
+function util.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
+
+-- Integer validation
+function util.is_int(str)
+ if type(str) == "number" then
+ return math.floor(str) == str
+ elseif type(str) ~= "string" then
+ return false
+ end
+
+ return str:match("^-?%d+$") ~= nil
+end
+
+-- Float validation
+function util.is_float(str)
+ if type(str) == "number" then
+ return true
+ elseif type(str) ~= "string" then
+ return false
+ end
+
+ return str:match("^-?%d+%.?%d*$") ~= nil
+end
+
+-- Boolean validation
+function util.is_bool(value)
+ if type(value) == "boolean" then
+ return true
+ 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
+ v == 1 or v == 0
+end
+
+-- Convert to boolean
+function util.to_bool(value)
+ if type(value) == "boolean" then
+ return 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
+
+-- Sanitize string (simple version)
+function util.sanitize_string(str)
+ if type(str) ~= "string" then
+ return ""
+ end
+
+ return util.html_special_chars(str)
+end
+
+-- Sanitize to integer
+function util.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
+end
+
+-- Sanitize to float
+function util.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
+end
+
+-- Sanitize URL
+function util.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 util.is_url(str) then
+ return str
+ end
+
+ -- Try to prepend http:// if it's missing
+ if not str:match("^https?://") and util.is_url("http://" .. str) then
+ return "http://" .. str
+ end
+
+ return ""
+end
+
+-- Sanitize email
+function util.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 util.is_email(str) then
+ return str
+ end
+
+ return ""
+end
+
+-- ======================================================================
+-- SECURITY FUNCTIONS
+-- ======================================================================
+
+-- Basic XSS prevention
+function util.xss_clean(str)
+ if type(str) ~= "string" then
+ return str
+ end
+
+ -- Convert problematic characters to entities
+ local result = util.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
+
+-- Base64 encode
+function util.base64_encode(str)
+ if type(str) ~= "string" then
+ return str
+ end
+
+ return __base64_encode(str)
+end
+
+-- Base64 decode
+function util.base64_decode(str)
+ if type(str) ~= "string" then
+ return str
+ end
+
+ return __base64_decode(str)
+end
+
+return util
\ No newline at end of file