Compare commits
No commits in common. "7397c3ebbc4e09687f8e8edacb31cb4604b3ae33" and "e05369431cfd76e405b84e2a5d3141fa23782ed0" have entirely different histories.
7397c3ebbc
...
e05369431c
@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 "*"
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
175
tests/crypto.lua
175
tests/crypto.lua
@ -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
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
|
|||||||
@ -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
3
todo_sessions.json
Normal 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}]}"
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user