Compare commits

..

No commits in common. "7397c3ebbc4e09687f8e8edacb31cb4604b3ae33" and "e05369431cfd76e405b84e2a5d3141fa23782ed0" have entirely different histories.

7 changed files with 40 additions and 1048 deletions

View File

@ -9,16 +9,10 @@ import (
"crypto/sha512" "crypto/sha512"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"fmt"
"math/big" "math/big"
"strings"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go" luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
"github.com/google/uuid" "github.com/google/uuid"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/scrypt"
) )
func GetFunctionList() map[string]luajit.GoFunction { func GetFunctionList() map[string]luajit.GoFunction {
@ -42,16 +36,6 @@ func GetFunctionList() map[string]luajit.GoFunction {
"random_hex": random_hex, "random_hex": random_hex,
"random_string": random_string, "random_string": random_string,
"secure_compare": secure_compare, "secure_compare": secure_compare,
"argon2_hash": argon2_hash,
"argon2_verify": argon2_verify,
"bcrypt_hash": bcrypt_hash,
"bcrypt_verify": bcrypt_verify,
"scrypt_hash": scrypt_hash,
"scrypt_verify": scrypt_verify,
"pbkdf2_hash": pbkdf2_hash,
"pbkdf2_verify": pbkdf2_verify,
"password_hash": password_hash,
"password_verify": password_verify,
} }
} }
@ -251,298 +235,3 @@ func secure_compare(s *luajit.State) int {
s.PushBoolean(hmac.Equal([]byte(a), []byte(b))) s.PushBoolean(hmac.Equal([]byte(a), []byte(b)))
return 1 return 1
} }
func argon2_hash(s *luajit.State) int {
password := s.ToString(1)
time := uint32(1)
memory := uint32(64 * 1024)
threads := uint8(4)
keyLen := uint32(32)
if s.GetTop() >= 2 && !s.IsNil(2) {
time = uint32(s.ToNumber(2))
}
if s.GetTop() >= 3 && !s.IsNil(3) {
memory = uint32(s.ToNumber(3))
}
if s.GetTop() >= 4 && !s.IsNil(4) {
threads = uint8(s.ToNumber(4))
}
if s.GetTop() >= 5 && !s.IsNil(5) {
keyLen = uint32(s.ToNumber(5))
}
salt := make([]byte, 16)
if _, err := rand.Read(salt); err != nil {
s.PushNil()
s.PushString("failed to generate salt")
return 2
}
hash := argon2.IDKey([]byte(password), salt, time, memory, threads, keyLen)
encodedSalt := base64.RawStdEncoding.EncodeToString(salt)
encodedHash := base64.RawStdEncoding.EncodeToString(hash)
result := fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s",
memory, time, threads, encodedSalt, encodedHash)
s.PushString(result)
return 1
}
func argon2_verify(s *luajit.State) int {
password := s.ToString(1)
hash := s.ToString(2)
parts := strings.Split(hash, "$")
if len(parts) != 6 || parts[1] != "argon2id" {
s.PushBoolean(false)
return 1
}
var memory, time uint32
var threads uint8
if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, &threads); err != nil {
s.PushBoolean(false)
return 1
}
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
s.PushBoolean(false)
return 1
}
expectedHash, err := base64.RawStdEncoding.DecodeString(parts[5])
if err != nil {
s.PushBoolean(false)
return 1
}
actualHash := argon2.IDKey([]byte(password), salt, time, memory, threads, uint32(len(expectedHash)))
s.PushBoolean(hmac.Equal(actualHash, expectedHash))
return 1
}
func bcrypt_hash(s *luajit.State) int {
password := s.ToString(1)
cost := 12
if s.GetTop() >= 2 && !s.IsNil(2) {
cost = int(s.ToNumber(2))
if cost < 4 || cost > 31 {
s.PushNil()
s.PushString("invalid cost (must be 4-31)")
return 2
}
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), cost)
if err != nil {
s.PushNil()
s.PushString("bcrypt hash failed")
return 2
}
s.PushString(string(hash))
return 1
}
func bcrypt_verify(s *luajit.State) int {
password := s.ToString(1)
hash := s.ToString(2)
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
s.PushBoolean(err == nil)
return 1
}
func scrypt_hash(s *luajit.State) int {
password := s.ToString(1)
N := 32768 // CPU cost
r := 8 // block size
p := 1 // parallelization
keyLen := 32 // key length
if s.GetTop() >= 2 && !s.IsNil(2) {
N = int(s.ToNumber(2))
}
if s.GetTop() >= 3 && !s.IsNil(3) {
r = int(s.ToNumber(3))
}
if s.GetTop() >= 4 && !s.IsNil(4) {
p = int(s.ToNumber(4))
}
if s.GetTop() >= 5 && !s.IsNil(5) {
keyLen = int(s.ToNumber(5))
}
salt := make([]byte, 16)
if _, err := rand.Read(salt); err != nil {
s.PushNil()
s.PushString("failed to generate salt")
return 2
}
hash, err := scrypt.Key([]byte(password), salt, N, r, p, keyLen)
if err != nil {
s.PushNil()
s.PushString("scrypt hash failed")
return 2
}
encodedSalt := base64.RawStdEncoding.EncodeToString(salt)
encodedHash := base64.RawStdEncoding.EncodeToString(hash)
result := fmt.Sprintf("$scrypt$N=%d,r=%d,p=%d$%s$%s", N, r, p, encodedSalt, encodedHash)
s.PushString(result)
return 1
}
func scrypt_verify(s *luajit.State) int {
password := s.ToString(1)
hash := s.ToString(2)
parts := strings.Split(hash, "$")
if len(parts) != 5 || parts[1] != "scrypt" {
s.PushBoolean(false)
return 1
}
var N, r, p int
if _, err := fmt.Sscanf(parts[2], "N=%d,r=%d,p=%d", &N, &r, &p); err != nil {
s.PushBoolean(false)
return 1
}
salt, err := base64.RawStdEncoding.DecodeString(parts[3])
if err != nil {
s.PushBoolean(false)
return 1
}
expectedHash, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
s.PushBoolean(false)
return 1
}
actualHash, err := scrypt.Key([]byte(password), salt, N, r, p, len(expectedHash))
if err != nil {
s.PushBoolean(false)
return 1
}
s.PushBoolean(hmac.Equal(actualHash, expectedHash))
return 1
}
func pbkdf2_hash(s *luajit.State) int {
password := s.ToString(1)
iterations := 100000
keyLen := 32
if s.GetTop() >= 2 && !s.IsNil(2) {
iterations = int(s.ToNumber(2))
}
if s.GetTop() >= 3 && !s.IsNil(3) {
keyLen = int(s.ToNumber(3))
}
salt := make([]byte, 16)
if _, err := rand.Read(salt); err != nil {
s.PushNil()
s.PushString("failed to generate salt")
return 2
}
hash := pbkdf2.Key([]byte(password), salt, iterations, keyLen, sha256.New)
encodedSalt := base64.RawStdEncoding.EncodeToString(salt)
encodedHash := base64.RawStdEncoding.EncodeToString(hash)
result := fmt.Sprintf("$pbkdf2-sha256$i=%d$%s$%s", iterations, encodedSalt, encodedHash)
s.PushString(result)
return 1
}
func pbkdf2_verify(s *luajit.State) int {
password := s.ToString(1)
hash := s.ToString(2)
parts := strings.Split(hash, "$")
if len(parts) != 5 || parts[1] != "pbkdf2-sha256" {
s.PushBoolean(false)
return 1
}
var iterations int
if _, err := fmt.Sscanf(parts[2], "i=%d", &iterations); err != nil {
s.PushBoolean(false)
return 1
}
salt, err := base64.RawStdEncoding.DecodeString(parts[3])
if err != nil {
s.PushBoolean(false)
return 1
}
expectedHash, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
s.PushBoolean(false)
return 1
}
actualHash := pbkdf2.Key([]byte(password), salt, iterations, len(expectedHash), sha256.New)
s.PushBoolean(hmac.Equal(actualHash, expectedHash))
return 1
}
func password_hash(s *luajit.State) int {
password := s.ToString(1)
algorithm := "argon2id" // default
if s.GetTop() >= 2 && !s.IsNil(2) {
algorithm = s.ToString(2)
}
switch algorithm {
case "argon2id":
s.PushString(password)
return argon2_hash(s)
case "bcrypt":
s.PushString(password)
if s.GetTop() >= 3 {
s.PushNumber(s.ToNumber(3))
}
return bcrypt_hash(s)
case "scrypt":
s.PushString(password)
return scrypt_hash(s)
case "pbkdf2":
s.PushString(password)
return pbkdf2_hash(s)
default:
s.PushNil()
s.PushString("unsupported algorithm: " + algorithm)
return 2
}
}
func password_verify(s *luajit.State) int {
hash := s.ToString(2)
// Auto-detect algorithm from hash format
if strings.HasPrefix(hash, "$argon2id$") {
return argon2_verify(s)
} else if strings.HasPrefix(hash, "$2a$") || strings.HasPrefix(hash, "$2b$") || strings.HasPrefix(hash, "$2y$") {
return bcrypt_verify(s)
} else if strings.HasPrefix(hash, "$scrypt$") {
return scrypt_verify(s)
} else if strings.HasPrefix(hash, "$pbkdf2-sha256$") {
return pbkdf2_verify(s)
}
s.PushBoolean(false)
return 1
}

View File

@ -1,3 +1,4 @@
local json = require("json")
local crypto = {} local crypto = {}
-- ====================================================================== -- ======================================================================
@ -365,166 +366,4 @@ function crypto.verify_integrity(check)
return crypto.secure_compare(expected, check.hash) return crypto.secure_compare(expected, check.hash)
end end
-- ======================================================================
-- PASSWORD HASHING
-- ======================================================================
-- Generic password hashing (defaults to argon2id)
function crypto.hash_password(password, algorithm, options)
algorithm = algorithm or "argon2id"
options = options or {}
local result, err
if algorithm == "argon2id" then
local time = options.time or 1
local memory = options.memory or 65536 -- 64MB in KB
local threads = options.threads or 4
local keylen = options.keylen or 32
result, err = moonshark.argon2_hash(password, time, memory, threads, keylen)
elseif algorithm == "bcrypt" then
local cost = options.cost or 12
result, err = moonshark.bcrypt_hash(password, cost)
elseif algorithm == "scrypt" then
local N = options.N or 32768
local r = options.r or 8
local p = options.p or 1
local keylen = options.keylen or 32
result, err = moonshark.scrypt_hash(password, N, r, p, keylen)
elseif algorithm == "pbkdf2" then
local iterations = options.iterations or 100000
local keylen = options.keylen or 32
result, err = moonshark.pbkdf2_hash(password, iterations, keylen)
else
error("unsupported algorithm: " .. algorithm)
end
if not result then
error(err)
end
return result
end
-- Generic password verification (auto-detects algorithm)
function crypto.verify_password(password, hash)
return moonshark.password_verify(password, hash)
end
-- ======================================================================
-- ALGORITHM-SPECIFIC FUNCTIONS
-- ======================================================================
-- Argon2id hashing
function crypto.argon2_hash(password, options)
options = options or {}
local time = options.time or 1
local memory = options.memory or 65536
local threads = options.threads or 4
local keylen = options.keylen or 32
local result, err = moonshark.argon2_hash(password, time, memory, threads, keylen)
if not result then error(err) end
return result
end
function crypto.argon2_verify(password, hash)
return moonshark.argon2_verify(password, hash)
end
-- bcrypt hashing
function crypto.bcrypt_hash(password, cost)
cost = cost or 12
local result, err = moonshark.bcrypt_hash(password, cost)
if not result then error(err) end
return result
end
function crypto.bcrypt_verify(password, hash)
return moonshark.bcrypt_verify(password, hash)
end
-- scrypt hashing
function crypto.scrypt_hash(password, options)
options = options or {}
local N = options.N or 32768
local r = options.r or 8
local p = options.p or 1
local keylen = options.keylen or 32
local result, err = moonshark.scrypt_hash(password, N, r, p, keylen)
if not result then error(err) end
return result
end
function crypto.scrypt_verify(password, hash)
return moonshark.scrypt_verify(password, hash)
end
-- PBKDF2 hashing
function crypto.pbkdf2_hash(password, iterations, keylen)
iterations = iterations or 100000
keylen = keylen or 32
local result, err = moonshark.pbkdf2_hash(password, iterations, keylen)
if not result then error(err) end
return result
end
function crypto.pbkdf2_verify(password, hash)
return moonshark.pbkdf2_verify(password, hash)
end
-- ======================================================================
-- PASSWORD CONFIG PRESETS
-- ======================================================================
function crypto.hash_password_fast(password, algorithm)
algorithm = algorithm or "argon2id"
local options = {
argon2id = { time = 1, memory = 8192, threads = 1 },
bcrypt = { cost = 10 },
scrypt = { N = 16384, r = 8, p = 1 },
pbkdf2 = { iterations = 50000 }
}
return crypto.hash_password(password, algorithm, options[algorithm])
end
function crypto.hash_password_strong(password, algorithm)
algorithm = algorithm or "argon2id"
local options = {
argon2id = { time = 3, memory = 131072, threads = 4 },
bcrypt = { cost = 14 },
scrypt = { N = 65536, r = 8, p = 2 },
pbkdf2 = { iterations = 200000 }
}
return crypto.hash_password(password, algorithm, options[algorithm])
end
-- ======================================================================
-- UTILITY FUNCTIONS
-- ======================================================================
-- Detect algorithm from hash
function crypto.detect_algorithm(hash)
if hash:match("^%$argon2id%$") then
return "argon2id"
elseif hash:match("^%$2[aby]%$") then
return "bcrypt"
elseif hash:match("^%$scrypt%$") then
return "scrypt"
elseif hash:match("^%$pbkdf2%-sha256%$") then
return "pbkdf2"
else
return "unknown"
end
end
return crypto return crypto

View File

@ -195,106 +195,6 @@ function Session:regenerate()
return self return self
end end
-- ======================================================================
-- ROUTER CLASS
-- ======================================================================
local Router = {}
Router.__index = Router
function Router.new()
return setmetatable({
_middleware = {},
_prefix = ""
}, Router)
end
function Router:use(...)
local args = {...}
if #args == 1 and type(args[1]) == "function" then
table.insert(self._middleware, args[1])
elseif #args == 2 and type(args[1]) == "string" and type(args[2]) == "function" then
-- Path-specific middleware for this router
table.insert(self._middleware, {path = args[1], handler = args[2]})
else
error("Invalid arguments to use()")
end
return self
end
function Router:group(path_prefix, callback)
local group_router = Router.new()
group_router._prefix = self._prefix .. path_prefix
-- Inherit parent middleware
for _, mw in ipairs(self._middleware) do
table.insert(group_router._middleware, mw)
end
if callback then
callback(group_router)
end
return group_router
end
function Router:_add_route(method, path, handler)
if not string.starts_with(path, "/") then
path = "/" .. path
end
local full_path = self._prefix .. path
local segments = split_path(full_path)
-- Create middleware chain for this specific route
local route_middleware = {}
-- Add global middleware first
for _, mw in ipairs(_G._http_middleware) do
if mw.path == nil or string.starts_with(full_path, mw.path) then
table.insert(route_middleware, mw.handler)
end
end
-- Add router middleware
for _, mw in ipairs(self._middleware) do
if type(mw) == "function" then
table.insert(route_middleware, mw)
elseif type(mw) == "table" and mw.path then
-- Check if path-specific middleware applies
if string.starts_with(full_path, mw.path) then
table.insert(route_middleware, mw.handler)
end
end
end
table.insert(_G._http_routes, {
method = method,
path = full_path,
segments = segments,
handler = handler,
middleware = route_middleware -- Store middleware per route
})
return self
end
-- HTTP method helpers for Router
function Router:get(path, handler) return self:_add_route("GET", path, handler) end
function Router:post(path, handler) return self:_add_route("POST", path, handler) end
function Router:put(path, handler) return self:_add_route("PUT", path, handler) end
function Router:delete(path, handler) return self:_add_route("DELETE", path, handler) end
function Router:patch(path, handler) return self:_add_route("PATCH", path, handler) end
function Router:head(path, handler) return self:_add_route("HEAD", path, handler) end
function Router:options(path, handler) return self:_add_route("OPTIONS", path, handler) end
function Router:all(path, handler)
local methods = {"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
for _, method in ipairs(methods) do
self:_add_route(method, path, handler)
end
return self
end
-- ====================================================================== -- ======================================================================
-- REQUEST CLASS -- REQUEST CLASS
-- ====================================================================== -- ======================================================================
@ -591,15 +491,12 @@ local function match_route(method, path)
return nil, {} return nil, {}
end end
-- ======================================================================
-- OPTIMIZED REQUEST HANDLING
-- ======================================================================
function _http_handle_request(req_table, res_table) function _http_handle_request(req_table, res_table)
local res = Response.new(res_table) local res = Response.new(res_table)
local req = Request.new(req_table, res) local req = Request.new(req_table, res)
-- Fast route lookup local function run_middleware(index)
if index > #_G._http_middleware then
local route, params = match_route(req.method, req.path) local route, params = match_route(req.method, req.path)
req.params = params req.params = params
@ -608,27 +505,18 @@ function _http_handle_request(req_table, res_table)
return return
end end
-- Fast path: no middleware
if not route.middleware or #route.middleware == 0 then
route.handler(req, res)
-- Auto-save dirty sessions
if req.session._loaded and req.session._dirty then
req.session:save()
end
return
end
-- Run route-specific middleware chain
local function run_middleware(index)
if index > #route.middleware then
route.handler(req, res) route.handler(req, res)
return return
end end
route.middleware[index](req, res, function() local mw = _G._http_middleware[index]
if mw.path == nil or string.starts_with(req.path, mw.path) then
mw.handler(req, res, function()
run_middleware(index + 1) run_middleware(index + 1)
end) end)
else
run_middleware(index + 1)
end
end end
run_middleware(1) run_middleware(1)
@ -697,18 +585,7 @@ function Server:configure_sessions(options)
return self return self
end end
function Server:group(path_prefix, callback)
local router = Router.new()
router._prefix = path_prefix
if callback then
callback(router)
end
return router
end
function Server:use(...) function Server:use(...)
-- Global middleware - stored per route during registration
local args = {...} local args = {...}
if #args == 1 and type(args[1]) == "function" then if #args == 1 and type(args[1]) == "function" then
table.insert(_G._http_middleware, {path = nil, handler = args[1]}) table.insert(_G._http_middleware, {path = nil, handler = args[1]})
@ -717,6 +594,7 @@ function Server:use(...)
else else
error("Invalid arguments to use()") error("Invalid arguments to use()")
end end
return self return self
end end
@ -727,20 +605,11 @@ function Server:_add_route(method, path, handler)
local segments = split_path(path) local segments = split_path(path)
-- Apply global middleware to this route
local route_middleware = {}
for _, mw in ipairs(_G._http_middleware) do
if mw.path == nil or string.starts_with(path, mw.path) then
table.insert(route_middleware, mw.handler)
end
end
table.insert(_G._http_routes, { table.insert(_G._http_routes, {
method = method, method = method,
path = path, path = path,
segments = segments, segments = segments,
handler = handler, handler = handler
middleware = route_middleware
}) })
return self return self
@ -802,10 +671,6 @@ end
-- MIDDLEWARE HELPERS -- MIDDLEWARE HELPERS
-- ====================================================================== -- ======================================================================
function http.router()
return Router.new()
end
function http.cors(options) function http.cors(options)
options = options or {} options = options or {}
local origin = options.origin or "*" local origin = options.origin or "*"

View File

@ -3,7 +3,6 @@ local orig_remove = table.remove
local orig_concat = table.concat local orig_concat = table.concat
local orig_sort = table.sort local orig_sort = table.sort
-- Enhanced table.insert with validation
function table.insert(t, pos, value) function table.insert(t, pos, value)
if type(t) ~= "table" then error("table.insert: first argument must be a table", 2) end if type(t) ~= "table" then error("table.insert: first argument must be a table", 2) end
@ -19,7 +18,6 @@ function table.insert(t, pos, value)
end end
end end
-- Enhanced table.remove with validation
function table.remove(t, pos) function table.remove(t, pos)
if type(t) ~= "table" then error("table.remove: first argument must be a table", 2) end if type(t) ~= "table" then error("table.remove: first argument must be a table", 2) end
if pos ~= nil and (type(pos) ~= "number" or pos ~= math.floor(pos)) then if pos ~= nil and (type(pos) ~= "number" or pos ~= math.floor(pos)) then
@ -28,7 +26,6 @@ function table.remove(t, pos)
return orig_remove(t, pos) return orig_remove(t, pos)
end end
-- Enhanced table.concat with validation
function table.concat(t, sep, start_idx, end_idx) function table.concat(t, sep, start_idx, end_idx)
if type(t) ~= "table" then error("table.concat: first argument must be a table", 2) end if type(t) ~= "table" then error("table.concat: first argument must be a table", 2) end
if sep ~= nil and type(sep) ~= "string" then error("table.concat: separator must be a string", 2) end if sep ~= nil and type(sep) ~= "string" then error("table.concat: separator must be a string", 2) end
@ -41,24 +38,17 @@ function table.concat(t, sep, start_idx, end_idx)
return orig_concat(t, sep, start_idx, end_idx) return orig_concat(t, sep, start_idx, end_idx)
end end
-- Enhanced table.sort with validation
function table.sort(t, comp) function table.sort(t, comp)
if type(t) ~= "table" then error("table.sort: first argument must be a table", 2) end if type(t) ~= "table" then error("table.sort: first argument must be a table", 2) end
if comp ~= nil and type(comp) ~= "function" then error("table.sort: comparator must be a function", 2) end if comp ~= nil and type(comp) ~= "function" then error("table.sort: comparator must be a function", 2) end
orig_sort(t, comp) orig_sort(t, comp)
end end
--- Returns the length of an array-like table
-- @param t table to measure
-- @return number of array elements
function table.length(t) function table.length(t)
if type(t) ~= "table" then error("table.length: argument must be a table", 2) end if type(t) ~= "table" then error("table.length: argument must be a table", 2) end
return #t return #t
end end
--- Returns the total number of key-value pairs in a table
-- @param t table to count
-- @return total number of elements
function table.size(t) function table.size(t)
if type(t) ~= "table" then error("table.size: argument must be a table", 2) end if type(t) ~= "table" then error("table.size: argument must be a table", 2) end
local count = 0 local count = 0
@ -68,17 +58,11 @@ function table.size(t)
return count return count
end end
--- Checks if a table is empty
-- @param t table to check
-- @return true if table has no elements
function table.is_empty(t) function table.is_empty(t)
if type(t) ~= "table" then error("table.is_empty: argument must be a table", 2) end if type(t) ~= "table" then error("table.is_empty: argument must be a table", 2) end
return next(t) == nil return next(t) == nil
end end
--- Checks if a table is an array (contiguous integer keys starting from 1)
-- @param t table to check
-- @return true if table is an array
function table.is_array(t) function table.is_array(t)
if type(t) ~= "table" then error("table.is_array: argument must be a table", 2) end if type(t) ~= "table" then error("table.is_array: argument must be a table", 2) end
if table.is_empty(t) then return true end if table.is_empty(t) then return true end
@ -95,8 +79,6 @@ function table.is_array(t)
return max_index == count return max_index == count
end end
--- Removes all elements from a table
-- @param t table to clear
function table.clear(t) function table.clear(t)
if type(t) ~= "table" then error("table.clear: argument must be a table", 2) end if type(t) ~= "table" then error("table.clear: argument must be a table", 2) end
for k in pairs(t) do for k in pairs(t) do
@ -104,9 +86,6 @@ function table.clear(t)
end end
end end
--- Creates a shallow copy of a table
-- @param t table to clone
-- @return new table with same key-value pairs
function table.clone(t) function table.clone(t)
if type(t) ~= "table" then error("table.clone: argument must be a table", 2) end if type(t) ~= "table" then error("table.clone: argument must be a table", 2) end
local result = {} local result = {}
@ -116,9 +95,6 @@ function table.clone(t)
return result return result
end end
--- Creates a deep copy of a table, handling circular references
-- @param t table to deep copy
-- @return new table with recursively copied values
function table.deep_copy(t) function table.deep_copy(t)
if type(t) ~= "table" then error("table.deep_copy: argument must be a table", 2) end if type(t) ~= "table" then error("table.deep_copy: argument must be a table", 2) end
@ -139,10 +115,6 @@ function table.deep_copy(t)
return copy_recursive(t, {}) return copy_recursive(t, {})
end end
--- Checks if a table contains a specific value
-- @param t table to search
-- @param value value to find
-- @return true if value is found
function table.contains(t, value) function table.contains(t, value)
if type(t) ~= "table" then error("table.contains: first argument must be a table", 2) end if type(t) ~= "table" then error("table.contains: first argument must be a table", 2) end
for _, v in pairs(t) do for _, v in pairs(t) do
@ -151,10 +123,6 @@ function table.contains(t, value)
return false return false
end end
--- Returns the first key that maps to the given value
-- @param t table to search
-- @param value value to find
-- @return key that maps to value, or nil if not found
function table.index_of(t, value) function table.index_of(t, value)
if type(t) ~= "table" then error("table.index_of: first argument must be a table", 2) end if type(t) ~= "table" then error("table.index_of: first argument must be a table", 2) end
for k, v in pairs(t) do for k, v in pairs(t) do
@ -163,10 +131,6 @@ function table.index_of(t, value)
return nil return nil
end end
--- Finds the first element that satisfies a predicate
-- @param t table to search
-- @param predicate function(value, key, table) -> boolean
-- @return value, key of first matching element, or nil
function table.find(t, predicate) function table.find(t, predicate)
if type(t) ~= "table" then error("table.find: first argument must be a table", 2) end if type(t) ~= "table" then error("table.find: first argument must be a table", 2) end
if type(predicate) ~= "function" then error("table.find: second argument must be a function", 2) end if type(predicate) ~= "function" then error("table.find: second argument must be a function", 2) end
@ -177,10 +141,6 @@ function table.find(t, predicate)
return nil return nil
end end
--- Finds the key of the first element that satisfies a predicate
-- @param t table to search
-- @param predicate function(value, key, table) -> boolean
-- @return key of first matching element, or nil
function table.find_index(t, predicate) function table.find_index(t, predicate)
if type(t) ~= "table" then error("table.find_index: first argument must be a table", 2) end if type(t) ~= "table" then error("table.find_index: first argument must be a table", 2) end
if type(predicate) ~= "function" then error("table.find_index: second argument must be a function", 2) end if type(predicate) ~= "function" then error("table.find_index: second argument must be a function", 2) end
@ -191,10 +151,6 @@ function table.find_index(t, predicate)
return nil return nil
end end
--- Counts elements matching a value or predicate
-- @param t table to search
-- @param value_or_predicate value to match or function(value, key, table) -> boolean
-- @return number of matching elements
function table.count(t, value_or_predicate) function table.count(t, value_or_predicate)
if type(t) ~= "table" then error("table.count: first argument must be a table", 2) end if type(t) ~= "table" then error("table.count: first argument must be a table", 2) end
@ -211,10 +167,6 @@ function table.count(t, value_or_predicate)
return count return count
end end
--- Filters elements that satisfy a predicate
-- @param t table to filter
-- @param predicate function(value, key, table) -> boolean
-- @return new table with elements that pass the test
function table.filter(t, predicate) function table.filter(t, predicate)
if type(t) ~= "table" then error("table.filter: first argument must be a table", 2) end if type(t) ~= "table" then error("table.filter: first argument must be a table", 2) end
if type(predicate) ~= "function" then error("table.filter: second argument must be a function", 2) end if type(predicate) ~= "function" then error("table.filter: second argument must be a function", 2) end
@ -243,10 +195,6 @@ function table.filter(t, predicate)
return result return result
end end
--- Filters elements that don't satisfy a predicate
-- @param t table to filter
-- @param predicate function(value, key, table) -> boolean
-- @return new table with elements that fail the test
function table.reject(t, predicate) function table.reject(t, predicate)
if type(t) ~= "table" then error("table.reject: first argument must be a table", 2) end if type(t) ~= "table" then error("table.reject: first argument must be a table", 2) end
if type(predicate) ~= "function" then error("table.reject: second argument must be a function", 2) end if type(predicate) ~= "function" then error("table.reject: second argument must be a function", 2) end
@ -254,10 +202,6 @@ function table.reject(t, predicate)
return table.filter(t, function(v, k, tbl) return not predicate(v, k, tbl) end) return table.filter(t, function(v, k, tbl) return not predicate(v, k, tbl) end)
end end
--- Transforms each element using a function
-- @param t table to transform
-- @param transformer function(value, key, table) -> new_value
-- @return new table with transformed elements
function table.map(t, transformer) function table.map(t, transformer)
if type(t) ~= "table" then error("table.map: first argument must be a table", 2) end if type(t) ~= "table" then error("table.map: first argument must be a table", 2) end
if type(transformer) ~= "function" then error("table.map: second argument must be a function", 2) end if type(transformer) ~= "function" then error("table.map: second argument must be a function", 2) end
@ -269,10 +213,6 @@ function table.map(t, transformer)
return result return result
end end
--- Transforms values while preserving keys
-- @param t table to transform
-- @param transformer function(value, key, table) -> new_value
-- @return new table with transformed values
function table.map_values(t, transformer) function table.map_values(t, transformer)
if type(t) ~= "table" then error("table.map_values: first argument must be a table", 2) end if type(t) ~= "table" then error("table.map_values: first argument must be a table", 2) end
if type(transformer) ~= "function" then error("table.map_values: second argument must be a function", 2) end if type(transformer) ~= "function" then error("table.map_values: second argument must be a function", 2) end
@ -284,10 +224,6 @@ function table.map_values(t, transformer)
return result return result
end end
--- Transforms keys while preserving values
-- @param t table to transform
-- @param transformer function(key, value, table) -> new_key
-- @return new table with transformed keys
function table.map_keys(t, transformer) function table.map_keys(t, transformer)
if type(t) ~= "table" then error("table.map_keys: first argument must be a table", 2) end if type(t) ~= "table" then error("table.map_keys: first argument must be a table", 2) end
if type(transformer) ~= "function" then error("table.map_keys: second argument must be a function", 2) end if type(transformer) ~= "function" then error("table.map_keys: second argument must be a function", 2) end
@ -300,11 +236,6 @@ function table.map_keys(t, transformer)
return result return result
end end
--- Reduces table to a single value using an accumulator function
-- @param t table to reduce
-- @param reducer function(accumulator, value, key, table) -> new_accumulator
-- @param initial optional initial accumulator value
-- @return final accumulator value
function table.reduce(t, reducer, initial) function table.reduce(t, reducer, initial)
if type(t) ~= "table" then error("table.reduce: first argument must be a table", 2) end if type(t) ~= "table" then error("table.reduce: first argument must be a table", 2) end
if type(reducer) ~= "function" then error("table.reduce: second argument must be a function", 2) end if type(reducer) ~= "function" then error("table.reduce: second argument must be a function", 2) end
@ -328,9 +259,6 @@ function table.reduce(t, reducer, initial)
return accumulator return accumulator
end end
--- Sums all numeric values in a table
-- @param t table containing numbers
-- @return sum of all values
function table.sum(t) function table.sum(t)
if type(t) ~= "table" then error("table.sum: argument must be a table", 2) end if type(t) ~= "table" then error("table.sum: argument must be a table", 2) end
local total = 0 local total = 0
@ -341,9 +269,6 @@ function table.sum(t)
return total return total
end end
--- Multiplies all numeric values in a table
-- @param t table containing numbers
-- @return product of all values
function table.product(t) function table.product(t)
if type(t) ~= "table" then error("table.product: argument must be a table", 2) end if type(t) ~= "table" then error("table.product: argument must be a table", 2) end
local result = 1 local result = 1
@ -354,9 +279,6 @@ function table.product(t)
return result return result
end end
--- Finds the minimum numeric value in a table
-- @param t table containing numbers
-- @return minimum value
function table.min(t) function table.min(t)
if type(t) ~= "table" then error("table.min: argument must be a table", 2) end if type(t) ~= "table" then error("table.min: argument must be a table", 2) end
if table.is_empty(t) then error("table.min: table is empty", 2) end if table.is_empty(t) then error("table.min: table is empty", 2) end
@ -371,9 +293,6 @@ function table.min(t)
return min_val return min_val
end end
--- Finds the maximum numeric value in a table
-- @param t table containing numbers
-- @return maximum value
function table.max(t) function table.max(t)
if type(t) ~= "table" then error("table.max: argument must be a table", 2) end if type(t) ~= "table" then error("table.max: argument must be a table", 2) end
if table.is_empty(t) then error("table.max: table is empty", 2) end if table.is_empty(t) then error("table.max: table is empty", 2) end
@ -388,19 +307,12 @@ function table.max(t)
return max_val return max_val
end end
--- Calculates the average of all numeric values
-- @param t table containing numbers
-- @return average value
function table.average(t) function table.average(t)
if type(t) ~= "table" then error("table.average: argument must be a table", 2) end if type(t) ~= "table" then error("table.average: argument must be a table", 2) end
if table.is_empty(t) then error("table.average: table is empty", 2) end if table.is_empty(t) then error("table.average: table is empty", 2) end
return table.sum(t) / table.size(t) return table.sum(t) / table.size(t)
end end
--- Tests if all elements satisfy a predicate or are truthy
-- @param t table to test
-- @param predicate optional function(value, key, table) -> boolean
-- @return true if all elements pass the test
function table.all(t, predicate) function table.all(t, predicate)
if type(t) ~= "table" then error("table.all: first argument must be a table", 2) end if type(t) ~= "table" then error("table.all: first argument must be a table", 2) end
@ -417,10 +329,6 @@ function table.all(t, predicate)
return true return true
end end
--- Tests if any element satisfies a predicate or is truthy
-- @param t table to test
-- @param predicate optional function(value, key, table) -> boolean
-- @return true if at least one element passes the test
function table.any(t, predicate) function table.any(t, predicate)
if type(t) ~= "table" then error("table.any: first argument must be a table", 2) end if type(t) ~= "table" then error("table.any: first argument must be a table", 2) end
@ -437,18 +345,11 @@ function table.any(t, predicate)
return false return false
end end
--- Tests if no element satisfies a predicate or is truthy
-- @param t table to test
-- @param predicate optional function(value, key, table) -> boolean
-- @return true if no elements pass the test
function table.none(t, predicate) function table.none(t, predicate)
if type(t) ~= "table" then error("table.none: first argument must be a table", 2) end if type(t) ~= "table" then error("table.none: first argument must be a table", 2) end
return not table.any(t, predicate) return not table.any(t, predicate)
end end
--- Removes duplicate values from a table
-- @param t table to deduplicate
-- @return new table with unique values
function table.unique(t) function table.unique(t)
if type(t) ~= "table" then error("table.unique: argument must be a table", 2) end if type(t) ~= "table" then error("table.unique: argument must be a table", 2) end
@ -474,10 +375,6 @@ function table.unique(t)
return result return result
end end
--- Returns elements common to both tables
-- @param t1 first table
-- @param t2 second table
-- @return new table with intersecting values
function table.intersection(t1, t2) function table.intersection(t1, t2)
if type(t1) ~= "table" then error("table.intersection: first argument must be a table", 2) end if type(t1) ~= "table" then error("table.intersection: first argument must be a table", 2) end
if type(t2) ~= "table" then error("table.intersection: second argument must be a table", 2) end if type(t2) ~= "table" then error("table.intersection: second argument must be a table", 2) end
@ -505,10 +402,6 @@ function table.intersection(t1, t2)
return result return result
end end
--- Combines two tables, keeping all unique values
-- @param t1 first table
-- @param t2 second table
-- @return new table with all unique values from both tables
function table.union(t1, t2) function table.union(t1, t2)
if type(t1) ~= "table" then error("table.union: first argument must be a table", 2) end if type(t1) ~= "table" then error("table.union: first argument must be a table", 2) end
if type(t2) ~= "table" then error("table.union: second argument must be a table", 2) end if type(t2) ~= "table" then error("table.union: second argument must be a table", 2) end
@ -532,10 +425,6 @@ function table.union(t1, t2)
return result return result
end end
--- Returns elements in first table but not in second
-- @param t1 first table
-- @param t2 second table
-- @return new table with values from t1 that are not in t2
function table.difference(t1, t2) function table.difference(t1, t2)
if type(t1) ~= "table" then error("table.difference: first argument must be a table", 2) end if type(t1) ~= "table" then error("table.difference: first argument must be a table", 2) end
if type(t2) ~= "table" then error("table.difference: second argument must be a table", 2) end if type(t2) ~= "table" then error("table.difference: second argument must be a table", 2) end
@ -548,9 +437,6 @@ function table.difference(t1, t2)
return table.filter(t1, function(v) return not set2[v] end) return table.filter(t1, function(v) return not set2[v] end)
end end
--- Reverses the order of elements in an array
-- @param t array to reverse
-- @return new array with elements in reverse order
function table.reverse(t) function table.reverse(t)
if type(t) ~= "table" then error("table.reverse: argument must be a table", 2) end if type(t) ~= "table" then error("table.reverse: argument must be a table", 2) end
if not table.is_array(t) then error("table.reverse: argument must be an array", 2) end if not table.is_array(t) then error("table.reverse: argument must be an array", 2) end
@ -563,9 +449,6 @@ function table.reverse(t)
return result return result
end end
--- Randomly shuffles elements in an array
-- @param t array to shuffle
-- @return new array with elements in random order
function table.shuffle(t) function table.shuffle(t)
if type(t) ~= "table" then error("table.shuffle: argument must be a table", 2) end if type(t) ~= "table" then error("table.shuffle: argument must be a table", 2) end
if not table.is_array(t) then error("table.shuffle: argument must be an array", 2) end if not table.is_array(t) then error("table.shuffle: argument must be an array", 2) end
@ -583,10 +466,6 @@ function table.shuffle(t)
return result return result
end end
--- Rotates array elements by a number of positions
-- @param t array to rotate
-- @param positions number of positions to rotate (positive = right, negative = left)
-- @return new array with rotated elements
function table.rotate(t, positions) function table.rotate(t, positions)
if type(t) ~= "table" then error("table.rotate: first argument must be a table", 2) end if type(t) ~= "table" then error("table.rotate: first argument must be a table", 2) end
if not table.is_array(t) then error("table.rotate: first argument must be an array", 2) end if not table.is_array(t) then error("table.rotate: first argument must be an array", 2) end
@ -609,11 +488,6 @@ function table.rotate(t, positions)
return result return result
end end
--- Extracts a section of an array
-- @param t array to slice
-- @param start_idx starting index (inclusive)
-- @param end_idx ending index (inclusive, optional)
-- @return new array containing the slice
function table.slice(t, start_idx, end_idx) function table.slice(t, start_idx, end_idx)
if type(t) ~= "table" then error("table.slice: first argument must be a table", 2) end if type(t) ~= "table" then error("table.slice: first argument must be a table", 2) end
if not table.is_array(t) then error("table.slice: first argument must be an array", 2) end if not table.is_array(t) then error("table.slice: first argument must be an array", 2) end
@ -639,12 +513,6 @@ function table.slice(t, start_idx, end_idx)
return result return result
end end
--- Modifies an array by removing elements and/or adding new ones
-- @param t array to modify
-- @param start_idx starting index for modification
-- @param delete_count number of elements to remove
-- @param ... elements to insert at start_idx
-- @return array of removed elements
function table.splice(t, start_idx, delete_count, ...) function table.splice(t, start_idx, delete_count, ...)
if type(t) ~= "table" then error("table.splice: first argument must be a table", 2) end if type(t) ~= "table" then error("table.splice: first argument must be a table", 2) end
if not table.is_array(t) then error("table.splice: first argument must be an array", 2) end if not table.is_array(t) then error("table.splice: first argument must be an array", 2) end
@ -696,10 +564,6 @@ function table.splice(t, start_idx, delete_count, ...)
return deleted return deleted
end end
--- Sorts an array by a key function
-- @param t array to sort
-- @param key_func function to extract sort key from each element
-- @return new sorted array
function table.sort_by(t, key_func) function table.sort_by(t, key_func)
if type(t) ~= "table" then error("table.sort_by: first argument must be a table", 2) end if type(t) ~= "table" then error("table.sort_by: first argument must be a table", 2) end
if not table.is_array(t) then error("table.sort_by: first argument must be an array", 2) end if not table.is_array(t) then error("table.sort_by: first argument must be an array", 2) end
@ -712,10 +576,6 @@ function table.sort_by(t, key_func)
return result return result
end end
--- Checks if an array is sorted according to a comparator
-- @param t array to check
-- @param comp optional comparator function
-- @return true if array is sorted
function table.is_sorted(t, comp) function table.is_sorted(t, comp)
if type(t) ~= "table" then error("table.is_sorted: first argument must be a table", 2) end if type(t) ~= "table" then error("table.is_sorted: first argument must be a table", 2) end
if not table.is_array(t) then error("table.is_sorted: first argument must be an array", 2) end if not table.is_array(t) then error("table.is_sorted: first argument must be an array", 2) end
@ -731,9 +591,6 @@ function table.is_sorted(t, comp)
return true return true
end end
--- Returns an array of all keys in a table
-- @param t table to extract keys from
-- @return array of keys
function table.keys(t) function table.keys(t)
if type(t) ~= "table" then error("table.keys: argument must be a table", 2) end if type(t) ~= "table" then error("table.keys: argument must be a table", 2) end
@ -744,9 +601,6 @@ function table.keys(t)
return result return result
end end
--- Returns an array of all values in a table
-- @param t table to extract values from
-- @return array of values
function table.values(t) function table.values(t)
if type(t) ~= "table" then error("table.values: argument must be a table", 2) end if type(t) ~= "table" then error("table.values: argument must be a table", 2) end
@ -757,9 +611,6 @@ function table.values(t)
return result return result
end end
--- Returns an array of [key, value] pairs
-- @param t table to convert to pairs
-- @return array of [key, value] arrays
function table.pairs(t) function table.pairs(t)
if type(t) ~= "table" then error("table.pairs: argument must be a table", 2) end if type(t) ~= "table" then error("table.pairs: argument must be a table", 2) end
@ -770,9 +621,6 @@ function table.pairs(t)
return result return result
end end
--- Merges multiple tables into a new table
-- @param ... tables to merge
-- @return new table with all key-value pairs
function table.merge(...) function table.merge(...)
local tables = {...} local tables = {...}
if #tables == 0 then return {} end if #tables == 0 then return {} end
@ -792,10 +640,6 @@ function table.merge(...)
return result return result
end end
--- Extends the first table with key-value pairs from other tables
-- @param t1 table to extend
-- @param ... tables to merge into t1
-- @return t1 (modified)
function table.extend(t1, ...) function table.extend(t1, ...)
if type(t1) ~= "table" then error("table.extend: first argument must be a table", 2) end if type(t1) ~= "table" then error("table.extend: first argument must be a table", 2) end
@ -814,9 +658,6 @@ function table.extend(t1, ...)
return t1 return t1
end end
--- Creates a table with keys and values swapped
-- @param t table to invert
-- @return new table with values as keys and keys as values
function table.invert(t) function table.invert(t)
if type(t) ~= "table" then error("table.invert: argument must be a table", 2) end if type(t) ~= "table" then error("table.invert: argument must be a table", 2) end
@ -827,10 +668,6 @@ function table.invert(t)
return result return result
end end
--- Creates a new table with only specified keys
-- @param t table to pick from
-- @param ... keys to include
-- @return new table with only specified keys
function table.pick(t, ...) function table.pick(t, ...)
if type(t) ~= "table" then error("table.pick: first argument must be a table", 2) end if type(t) ~= "table" then error("table.pick: first argument must be a table", 2) end
@ -846,10 +683,6 @@ function table.pick(t, ...)
return result return result
end end
--- Creates a new table excluding specified keys
-- @param t table to omit from
-- @param ... keys to exclude
-- @return new table without specified keys
function table.omit(t, ...) function table.omit(t, ...)
if type(t) ~= "table" then error("table.omit: first argument must be a table", 2) end if type(t) ~= "table" then error("table.omit: first argument must be a table", 2) end
@ -868,10 +701,6 @@ function table.omit(t, ...)
return result return result
end end
--- Deep equality comparison between two tables
-- @param t1 first table
-- @param t2 second table
-- @return true if tables are deeply equal
function table.deep_equals(t1, t2) function table.deep_equals(t1, t2)
if type(t1) ~= "table" then error("table.deep_equals: first argument must be a table", 2) end if type(t1) ~= "table" then error("table.deep_equals: first argument must be a table", 2) end
if type(t2) ~= "table" then error("table.deep_equals: second argument must be a table", 2) end if type(t2) ~= "table" then error("table.deep_equals: second argument must be a table", 2) end
@ -910,10 +739,6 @@ function table.deep_equals(t1, t2)
return equals_recursive(t1, t2, {}) return equals_recursive(t1, t2, {})
end end
--- Flattens nested arrays to specified depth
-- @param t array to flatten
-- @param depth maximum depth to flatten (default 1)
-- @return new flattened array
function table.flatten(t, depth) function table.flatten(t, depth)
if type(t) ~= "table" then error("table.flatten: first argument must be a table", 2) end if type(t) ~= "table" then error("table.flatten: first argument must be a table", 2) end
if not table.is_array(t) then error("table.flatten: first argument must be an array", 2) end if not table.is_array(t) then error("table.flatten: first argument must be an array", 2) end
@ -941,9 +766,6 @@ function table.flatten(t, depth)
return flatten_recursive(t, depth) return flatten_recursive(t, depth)
end end
--- Deep merges multiple tables, combining nested tables
-- @param ... tables to deep merge
-- @return new table with deeply merged contents
function table.deep_merge(...) function table.deep_merge(...)
local tables = {...} local tables = {...}
if #tables == 0 then return {} end if #tables == 0 then return {} end
@ -973,10 +795,6 @@ function table.deep_merge(...)
return result return result
end end
--- Splits an array into chunks of specified size
-- @param t array to chunk
-- @param size size of each chunk
-- @return array of chunks
function table.chunk(t, size) function table.chunk(t, size)
if type(t) ~= "table" then error("table.chunk: first argument must be a table", 2) end if type(t) ~= "table" then error("table.chunk: first argument must be a table", 2) end
if not table.is_array(t) then error("table.chunk: first argument must be an array", 2) end if not table.is_array(t) then error("table.chunk: first argument must be an array", 2) end
@ -998,10 +816,6 @@ function table.chunk(t, size)
return result return result
end end
--- Splits a table into two based on a predicate
-- @param t table to partition
-- @param predicate function(value, key, table) -> boolean
-- @return two tables: elements that pass and elements that fail
function table.partition(t, predicate) function table.partition(t, predicate)
if type(t) ~= "table" then error("table.partition: first argument must be a table", 2) end if type(t) ~= "table" then error("table.partition: first argument must be a table", 2) end
if type(predicate) ~= "function" then error("table.partition: second argument must be a function", 2) end if type(predicate) ~= "function" then error("table.partition: second argument must be a function", 2) end
@ -1029,10 +843,6 @@ function table.partition(t, predicate)
return truthy, falsy return truthy, falsy
end end
--- Groups table elements by a key function
-- @param t table to group
-- @param key_func function(value, key, table) -> group_key
-- @return table where keys are group keys and values are grouped elements
function table.group_by(t, key_func) function table.group_by(t, key_func)
if type(t) ~= "table" then error("table.group_by: first argument must be a table", 2) end if type(t) ~= "table" then error("table.group_by: first argument must be a table", 2) end
if type(key_func) ~= "function" then error("table.group_by: second argument must be a function", 2) end if type(key_func) ~= "function" then error("table.group_by: second argument must be a function", 2) end
@ -1055,9 +865,6 @@ function table.group_by(t, key_func)
return result return result
end end
--- Combines multiple arrays element-wise into tuples
-- @param ... arrays to zip together
-- @return array of tuples
function table.zip(...) function table.zip(...)
local arrays = {...} local arrays = {...}
if #arrays == 0 then error("table.zip: at least one argument required", 2) end if #arrays == 0 then error("table.zip: at least one argument required", 2) end
@ -1088,9 +895,6 @@ function table.zip(...)
return result return result
end end
--- Removes falsy values (nil and false) from a table
-- @param t table to compact
-- @return new table with only truthy values
function table.compact(t) function table.compact(t)
if type(t) ~= "table" then error("table.compact: argument must be a table", 2) end if type(t) ~= "table" then error("table.compact: argument must be a table", 2) end
@ -1121,10 +925,6 @@ function table.compact(t)
end end
end end
--- Returns a random sample of elements from an array
-- @param t array to sample from
-- @param n number of elements to sample (default 1)
-- @return array with sampled elements
function table.sample(t, n) function table.sample(t, n)
if type(t) ~= "table" then error("table.sample: first argument must be a table", 2) end if type(t) ~= "table" then error("table.sample: first argument must be a table", 2) end
if not table.is_array(t) then error("table.sample: first argument must be an array", 2) end if not table.is_array(t) then error("table.sample: first argument must be an array", 2) end
@ -1140,11 +940,6 @@ function table.sample(t, n)
return table.slice(shuffled, 1, n) return table.slice(shuffled, 1, n)
end end
--- Folds a table using an accumulator function
-- @param t table to fold
-- @param folder function(accumulator, value, key, table) -> new_accumulator
-- @param initial initial accumulator value
-- @return final accumulator value
function table.fold(t, folder, initial) function table.fold(t, folder, initial)
if type(t) ~= "table" then error("table.fold: first argument must be a table", 2) end if type(t) ~= "table" then error("table.fold: first argument must be a table", 2) end
if type(folder) ~= "function" then error("table.fold: second argument must be a function", 2) end if type(folder) ~= "function" then error("table.fold: second argument must be a function", 2) end

View File

@ -301,181 +301,6 @@ test("Error Handling", function()
assert_equal(crypto.is_uuid("12345"), false) assert_equal(crypto.is_uuid("12345"), false)
end) end)
-- ======================================================================
-- PASSWORD TESTS
-- ======================================================================
test("Password Hash and Verification", function()
local password = "hubba-ba-loo117!@#"
local hash = crypto.hash_password(password)
local hash_fast = crypto.hash_password_fast(password)
local hash_strong = crypto.hash_password_strong(password)
assert(crypto.verify_password(password, hash))
assert(crypto.verify_password(password, hash_fast))
assert(crypto.verify_password(password, hash_strong))
assert(not crypto.verify_password("failure", hash))
assert(not crypto.verify_password("failure", hash_fast))
assert(not crypto.verify_password("failure", hash_strong))
end)
test("Algorithm-Specific Password Hashing", function()
local password = "test123!@#"
-- Test each algorithm individually
local argon2_hash = crypto.hash_password(password, "argon2id")
local bcrypt_hash = crypto.hash_password(password, "bcrypt")
local scrypt_hash = crypto.hash_password(password, "scrypt")
local pbkdf2_hash = crypto.hash_password(password, "pbkdf2")
assert(crypto.verify_password(password, argon2_hash))
assert(crypto.verify_password(password, bcrypt_hash))
assert(crypto.verify_password(password, scrypt_hash))
assert(crypto.verify_password(password, pbkdf2_hash))
-- Verify wrong passwords fail
assert(not crypto.verify_password("wrong", argon2_hash))
assert(not crypto.verify_password("wrong", bcrypt_hash))
assert(not crypto.verify_password("wrong", scrypt_hash))
assert(not crypto.verify_password("wrong", pbkdf2_hash))
end)
test("Algorithm Detection", function()
local password = "detectme123"
local argon2_hash = crypto.hash_password(password, "argon2id")
local bcrypt_hash = crypto.hash_password(password, "bcrypt")
local scrypt_hash = crypto.hash_password(password, "scrypt")
local pbkdf2_hash = crypto.hash_password(password, "pbkdf2")
assert(crypto.detect_algorithm(argon2_hash) == "argon2id")
assert(crypto.detect_algorithm(bcrypt_hash) == "bcrypt")
assert(crypto.detect_algorithm(scrypt_hash) == "scrypt")
assert(crypto.detect_algorithm(pbkdf2_hash) == "pbkdf2")
assert(crypto.detect_algorithm("invalid$format") == "unknown")
end)
test("Custom Algorithm Options", function()
local password = "custom123"
-- Test custom argon2id options
local custom_argon2 = crypto.hash_password(password, "argon2id", {
time = 2,
memory = 32768,
threads = 2
})
assert(crypto.verify_password(password, custom_argon2))
-- Test custom bcrypt cost
local custom_bcrypt = crypto.hash_password(password, "bcrypt", {cost = 10})
assert(crypto.verify_password(password, custom_bcrypt))
-- Test custom scrypt parameters
local custom_scrypt = crypto.hash_password(password, "scrypt", {
N = 16384,
r = 4,
p = 2
})
assert(crypto.verify_password(password, custom_scrypt))
-- Test custom pbkdf2 iterations
local custom_pbkdf2 = crypto.hash_password(password, "pbkdf2", {
iterations = 50000
})
assert(crypto.verify_password(password, custom_pbkdf2))
end)
test("Direct Algorithm Functions", function()
local password = "direct123"
-- Test direct algorithm calls
local argon2_direct = crypto.argon2_hash(password)
local bcrypt_direct = crypto.bcrypt_hash(password)
local scrypt_direct = crypto.scrypt_hash(password)
local pbkdf2_direct = crypto.pbkdf2_hash(password)
assert(crypto.argon2_verify(password, argon2_direct))
assert(crypto.bcrypt_verify(password, bcrypt_direct))
assert(crypto.scrypt_verify(password, scrypt_direct))
assert(crypto.pbkdf2_verify(password, pbkdf2_direct))
-- Test with custom options
local argon2_custom = crypto.argon2_hash(password, {time = 1, memory = 16384})
local scrypt_custom = crypto.scrypt_hash(password, {N = 8192})
assert(crypto.argon2_verify(password, argon2_custom))
assert(crypto.scrypt_verify(password, scrypt_custom))
end)
test("Security Level Presets", function()
local password = "preset123"
local algorithms = {"argon2id", "bcrypt", "scrypt", "pbkdf2"}
for _, algo in ipairs(algorithms) do
local fast_hash = crypto.hash_password_fast(password, algo)
local strong_hash = crypto.hash_password_strong(password, algo)
assert(crypto.verify_password(password, fast_hash))
assert(crypto.verify_password(password, strong_hash))
-- Verify algorithm detection
assert(crypto.detect_algorithm(fast_hash) == algo)
assert(crypto.detect_algorithm(strong_hash) == algo)
end
end)
test("Edge Cases and Error Handling", function()
-- Test empty password
local empty_hash = crypto.hash_password("")
assert(crypto.verify_password("", empty_hash))
assert(not crypto.verify_password("not-empty", empty_hash))
-- Test long password
local long_password = string.rep("a", 1000)
local long_hash = crypto.hash_password(long_password)
assert(crypto.verify_password(long_password, long_hash))
-- Test unicode password
local unicode_password = "🔐password123🔑"
local unicode_hash = crypto.hash_password(unicode_password)
assert(crypto.verify_password(unicode_password, unicode_hash))
-- Test invalid hash formats
assert(not crypto.verify_password("test", "invalid-hash"))
assert(not crypto.verify_password("test", "$invalid$format$"))
-- Test unsupported algorithm error
local success, err = pcall(crypto.hash_password, "test", "invalid-algo")
assert(not success)
assert(string.find(err, "unsupported algorithm"))
end)
test("Cross-Algorithm Verification", function()
local password = "cross123"
-- Create hashes with different algorithms
local hashes = {
crypto.hash_password(password, "argon2id"),
crypto.hash_password(password, "bcrypt"),
crypto.hash_password(password, "scrypt"),
crypto.hash_password(password, "pbkdf2")
}
-- Each hash should only verify with correct password
for _, hash in ipairs(hashes) do
assert(crypto.verify_password(password, hash))
assert(not crypto.verify_password("wrong", hash))
end
-- Hashes should be different from each other
for i = 1, #hashes do
for j = i + 1, #hashes do
assert(hashes[i] ~= hashes[j])
end
end
end)
-- ====================================================================== -- ======================================================================
-- PERFORMANCE TESTS -- PERFORMANCE TESTS
-- ====================================================================== -- ======================================================================

View File

@ -883,29 +883,5 @@ test("Statistical Analysis", function()
assert_close(83.4, class_average, 0.1) assert_close(83.4, class_average, 0.1)
end) end)
test("Table Metatable Method Chaining", function()
local t = {1, 2, 3, 4, 5}
-- Check if methods are available directly on table instances
assert_equal("function", type(t.filter), "table should have filter method via metatable")
assert_equal("function", type(t.map), "table should have map method via metatable")
assert_equal("function", type(t.length), "table should have length method via metatable")
-- Test actual method chaining
local result = t:filter(function(v) return v > 2 end)
:map(function(v) return v * 2 end)
assert_table_equal({6, 8, 10}, result)
-- Test chaining with method calls
local nums = {1, 2, 3, 4, 5}
local sum = nums:sum()
assert_equal(15, sum)
local sizes = {a = 1, b = 2}
local size = sizes:size()
assert_equal(2, size)
end)
summary() summary()
test_exit() test_exit()

3
todo_sessions.json Normal file
View File

@ -0,0 +1,3 @@
{
"session:x5joQraQyEkfzzRMrcP4o8yK0xjgwtCW": "{\"todos\":[{\"text\":\"asdasd\",\"completed\":true,\"id\":\"1753414744_8147\",\"created_at\":1753414744},{\"text\":\"fsdf\",\"completed\":true,\"id\":\"1753414748_8147\",\"created_at\":1753414748},{\"text\":\"asdasd\",\"completed\":false,\"id\":\"1753415063_8147\",\"created_at\":1753415063},{\"id\":\"1753415066_8147\",\"completed\":false,\"text\":\"asdkjfhaslkjdhflkasjhdf\",\"created_at\":1753415066},{\"text\":\"alsdhnfpuihawepiufhbpioweHBFIOEWBSF\",\"completed\":false,\"id\":\"1753415069_8147\",\"created_at\":1753415069}]}"
}