crypto, fs, string libs

This commit is contained in:
Sky Johnson 2025-07-14 23:53:53 -05:00
parent acb8670177
commit 743fd0e835
14 changed files with 4058 additions and 87 deletions

1
.gitignore vendored
View File

@ -24,3 +24,4 @@ go.work
# Test directories and files # Test directories and files
test.lua test.lua
test_fs_dir

View File

@ -1,28 +1,35 @@
// functions/crypto.go
package functions package functions
import ( import (
"crypto/hmac"
"crypto/md5" "crypto/md5"
"crypto/rand"
"crypto/sha1"
"crypto/sha256" "crypto/sha256"
"crypto/sha512"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"fmt"
"math/big"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go" luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
"github.com/google/uuid"
) )
// GetCryptoFunctions returns all cryptographic Go functions
func GetCryptoFunctions() map[string]luajit.GoFunction { func GetCryptoFunctions() map[string]luajit.GoFunction {
return map[string]luajit.GoFunction{ return map[string]luajit.GoFunction{
"base64_encode": func(s *luajit.State) int { "base64_encode": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil { if err := s.CheckMinArgs(1); err != nil {
return s.PushError("base64_encode: %v", err) s.PushNil()
s.PushString(fmt.Sprintf("base64_encode: %v", err))
return 2
} }
str, err := s.SafeToString(1) str, err := s.SafeToString(1)
if err != nil { if err != nil {
return s.PushError("base64_encode: argument must be a string") s.PushNil()
s.PushString("base64_encode: argument must be a string")
return 2
} }
encoded := base64.StdEncoding.EncodeToString([]byte(str)) encoded := base64.StdEncoding.EncodeToString([]byte(str))
s.PushString(encoded) s.PushString(encoded)
return 1 return 1
@ -30,51 +37,358 @@ func GetCryptoFunctions() map[string]luajit.GoFunction {
"base64_decode": func(s *luajit.State) int { "base64_decode": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil { if err := s.CheckMinArgs(1); err != nil {
return s.PushError("base64_decode: %v", err) s.PushNil()
s.PushString(fmt.Sprintf("base64_decode: %v", err))
return 2
} }
str, err := s.SafeToString(1) str, err := s.SafeToString(1)
if err != nil { if err != nil {
return s.PushError("base64_decode: argument must be a string") s.PushNil()
s.PushString("base64_decode: argument must be a string")
return 2
} }
decoded, err := base64.StdEncoding.DecodeString(str) decoded, err := base64.StdEncoding.DecodeString(str)
if err != nil { if err != nil {
return s.PushError("base64_decode: %v", err) s.PushNil()
s.PushString(fmt.Sprintf("base64_decode: %v", err))
return 2
} }
s.PushString(string(decoded))
return 1
},
"base64_url_encode": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
s.PushNil()
s.PushString(fmt.Sprintf("base64_url_encode: %v", err))
return 2
}
str, err := s.SafeToString(1)
if err != nil {
s.PushNil()
s.PushString("base64_url_encode: argument must be a string")
return 2
}
encoded := base64.URLEncoding.EncodeToString([]byte(str))
s.PushString(encoded)
return 1
},
"base64_url_decode": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
s.PushNil()
s.PushString(fmt.Sprintf("base64_url_decode: %v", err))
return 2
}
str, err := s.SafeToString(1)
if err != nil {
s.PushNil()
s.PushString("base64_url_decode: argument must be a string")
return 2
}
decoded, err := base64.URLEncoding.DecodeString(str)
if err != nil {
s.PushNil()
s.PushString(fmt.Sprintf("base64_url_decode: %v", err))
return 2
}
s.PushString(string(decoded))
return 1
},
"hex_encode": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
s.PushNil()
s.PushString(fmt.Sprintf("hex_encode: %v", err))
return 2
}
str, err := s.SafeToString(1)
if err != nil {
s.PushNil()
s.PushString("hex_encode: argument must be a string")
return 2
}
encoded := hex.EncodeToString([]byte(str))
s.PushString(encoded)
return 1
},
"hex_decode": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
s.PushNil()
s.PushString(fmt.Sprintf("hex_decode: %v", err))
return 2
}
str, err := s.SafeToString(1)
if err != nil {
s.PushNil()
s.PushString("hex_decode: argument must be a string")
return 2
}
decoded, err := hex.DecodeString(str)
if err != nil {
s.PushNil()
s.PushString(fmt.Sprintf("hex_decode: %v", err))
return 2
}
s.PushString(string(decoded)) s.PushString(string(decoded))
return 1 return 1
}, },
"md5_hash": func(s *luajit.State) int { "md5_hash": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil { if err := s.CheckMinArgs(1); err != nil {
return s.PushError("md5_hash: %v", err) s.PushNil()
s.PushString(fmt.Sprintf("md5_hash: %v", err))
return 2
} }
str, err := s.SafeToString(1) str, err := s.SafeToString(1)
if err != nil { if err != nil {
return s.PushError("md5_hash: argument must be a string") s.PushNil()
s.PushString("md5_hash: argument must be a string")
return 2
} }
hash := md5.Sum([]byte(str)) hash := md5.Sum([]byte(str))
s.PushString(hex.EncodeToString(hash[:])) s.PushString(hex.EncodeToString(hash[:]))
return 1 return 1
}, },
"sha1_hash": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
s.PushNil()
s.PushString(fmt.Sprintf("sha1_hash: %v", err))
return 2
}
str, err := s.SafeToString(1)
if err != nil {
s.PushNil()
s.PushString("sha1_hash: argument must be a string")
return 2
}
hash := sha1.Sum([]byte(str))
s.PushString(hex.EncodeToString(hash[:]))
return 1
},
"sha256_hash": func(s *luajit.State) int { "sha256_hash": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil { if err := s.CheckMinArgs(1); err != nil {
return s.PushError("sha256_hash: %v", err) s.PushNil()
s.PushString(fmt.Sprintf("sha256_hash: %v", err))
return 2
} }
str, err := s.SafeToString(1) str, err := s.SafeToString(1)
if err != nil { if err != nil {
return s.PushError("sha256_hash: argument must be a string") s.PushNil()
s.PushString("sha256_hash: argument must be a string")
return 2
} }
hash := sha256.Sum256([]byte(str)) hash := sha256.Sum256([]byte(str))
s.PushString(hex.EncodeToString(hash[:])) s.PushString(hex.EncodeToString(hash[:]))
return 1 return 1
}, },
"sha512_hash": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
s.PushNil()
s.PushString(fmt.Sprintf("sha512_hash: %v", err))
return 2
}
str, err := s.SafeToString(1)
if err != nil {
s.PushNil()
s.PushString("sha512_hash: argument must be a string")
return 2
}
hash := sha512.Sum512([]byte(str))
s.PushString(hex.EncodeToString(hash[:]))
return 1
},
"hmac_sha256": func(s *luajit.State) int {
if err := s.CheckExactArgs(2); err != nil {
s.PushNil()
s.PushString(fmt.Sprintf("hmac_sha256: %v", err))
return 2
}
message, err := s.SafeToString(1)
if err != nil {
s.PushNil()
s.PushString("hmac_sha256: first argument must be a string")
return 2
}
key, err := s.SafeToString(2)
if err != nil {
s.PushNil()
s.PushString("hmac_sha256: second argument must be a string")
return 2
}
h := hmac.New(sha256.New, []byte(key))
h.Write([]byte(message))
s.PushString(hex.EncodeToString(h.Sum(nil)))
return 1
},
"hmac_sha1": func(s *luajit.State) int {
if err := s.CheckExactArgs(2); err != nil {
s.PushNil()
s.PushString(fmt.Sprintf("hmac_sha1: %v", err))
return 2
}
message, err := s.SafeToString(1)
if err != nil {
s.PushNil()
s.PushString("hmac_sha1: first argument must be a string")
return 2
}
key, err := s.SafeToString(2)
if err != nil {
s.PushNil()
s.PushString("hmac_sha1: second argument must be a string")
return 2
}
h := hmac.New(sha1.New, []byte(key))
h.Write([]byte(message))
s.PushString(hex.EncodeToString(h.Sum(nil)))
return 1
},
"uuid_generate": func(s *luajit.State) int {
id := uuid.New()
s.PushString(id.String())
return 1
},
"uuid_generate_v4": func(s *luajit.State) int {
id := uuid.New()
s.PushString(id.String())
return 1
},
"uuid_validate": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
s.PushBoolean(false)
return 1
}
str, err := s.SafeToString(1)
if err != nil {
s.PushBoolean(false)
return 1
}
_, err = uuid.Parse(str)
s.PushBoolean(err == nil)
return 1
},
"random_bytes": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
s.PushNil()
s.PushString(fmt.Sprintf("random_bytes: %v", err))
return 2
}
length, err := s.SafeToNumber(1)
if err != nil || length < 0 || length != float64(int(length)) {
s.PushNil()
s.PushString("random_bytes: argument must be a non-negative integer")
return 2
}
if length > 65536 {
s.PushNil()
s.PushString("random_bytes: length too large (max 65536)")
return 2
}
bytes := make([]byte, int(length))
if _, err := rand.Read(bytes); err != nil {
s.PushNil()
s.PushString("random_bytes: failed to generate random bytes")
return 2
}
s.PushString(string(bytes))
return 1
},
"random_hex": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
s.PushNil()
s.PushString(fmt.Sprintf("random_hex: %v", err))
return 2
}
length, err := s.SafeToNumber(1)
if err != nil || length < 0 || length != float64(int(length)) {
s.PushNil()
s.PushString("random_hex: argument must be a non-negative integer")
return 2
}
if length > 32768 {
s.PushNil()
s.PushString("random_hex: length too large (max 32768)")
return 2
}
bytes := make([]byte, int(length))
if _, err := rand.Read(bytes); err != nil {
s.PushNil()
s.PushString("random_hex: failed to generate random bytes")
return 2
}
s.PushString(hex.EncodeToString(bytes))
return 1
},
"random_string": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
s.PushNil()
s.PushString(fmt.Sprintf("random_string: %v", err))
return 2
}
length, err := s.SafeToNumber(1)
if err != nil || length < 0 || length != float64(int(length)) {
s.PushNil()
s.PushString("random_string: argument must be a non-negative integer")
return 2
}
if length > 65536 {
s.PushNil()
s.PushString("random_string: length too large (max 65536)")
return 2
}
charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
if s.GetTop() >= 2 {
if customCharset, err := s.SafeToString(2); err == nil {
charset = customCharset
}
}
result := make([]byte, int(length))
charsetLen := big.NewInt(int64(len(charset)))
for i := range result {
n, err := rand.Int(rand.Reader, charsetLen)
if err != nil {
s.PushNil()
s.PushString("random_string: failed to generate random number")
return 2
}
result[i] = charset[n.Int64()]
}
s.PushString(string(result))
return 1
},
"secure_compare": func(s *luajit.State) int {
if err := s.CheckExactArgs(2); err != nil {
s.PushBoolean(false)
return 1
}
a, err := s.SafeToString(1)
if err != nil {
s.PushBoolean(false)
return 1
}
b, err := s.SafeToString(2)
if err != nil {
s.PushBoolean(false)
return 1
}
s.PushBoolean(hmac.Equal([]byte(a), []byte(b)))
return 1
},
} }
} }

View File

@ -1,24 +1,26 @@
package functions package functions
import ( import (
"io"
"io/fs"
"os" "os"
"path/filepath"
"strings"
"time"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go" luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
) )
// GetFSFunctions returns all file system Go functions
func GetFSFunctions() map[string]luajit.GoFunction { func GetFSFunctions() map[string]luajit.GoFunction {
return map[string]luajit.GoFunction{ return map[string]luajit.GoFunction{
"file_exists": func(s *luajit.State) int { "file_exists": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil { if err := s.CheckMinArgs(1); err != nil {
return s.PushError("file_exists: %v", err) return s.PushError("file_exists: %v", err)
} }
path, err := s.SafeToString(1) path, err := s.SafeToString(1)
if err != nil { if err != nil {
return s.PushError("file_exists: argument must be a string") return s.PushError("file_exists: argument must be a string")
} }
_, err = os.Stat(path) _, err = os.Stat(path)
s.PushBoolean(err == nil) s.PushBoolean(err == nil)
return 1 return 1
@ -28,18 +30,15 @@ func GetFSFunctions() map[string]luajit.GoFunction {
if err := s.CheckMinArgs(1); err != nil { if err := s.CheckMinArgs(1); err != nil {
return s.PushError("file_size: %v", err) return s.PushError("file_size: %v", err)
} }
path, err := s.SafeToString(1) path, err := s.SafeToString(1)
if err != nil { if err != nil {
return s.PushError("file_size: argument must be a string") return s.PushError("file_size: argument must be a string")
} }
info, err := os.Stat(path) info, err := os.Stat(path)
if err != nil { if err != nil {
s.PushNumber(-1) s.PushNumber(-1)
return 1 return 1
} }
s.PushNumber(float64(info.Size())) s.PushNumber(float64(info.Size()))
return 1 return 1
}, },
@ -48,20 +47,532 @@ func GetFSFunctions() map[string]luajit.GoFunction {
if err := s.CheckMinArgs(1); err != nil { if err := s.CheckMinArgs(1); err != nil {
return s.PushError("file_is_dir: %v", err) return s.PushError("file_is_dir: %v", err)
} }
path, err := s.SafeToString(1) path, err := s.SafeToString(1)
if err != nil { if err != nil {
return s.PushError("file_is_dir: argument must be a string") return s.PushError("file_is_dir: argument must be a string")
} }
info, err := os.Stat(path) info, err := os.Stat(path)
if err != nil { if err != nil {
s.PushBoolean(false) s.PushBoolean(false)
return 1 return 1
} }
s.PushBoolean(info.IsDir()) s.PushBoolean(info.IsDir())
return 1 return 1
}, },
"file_read": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("file_read: %v", err)
}
path, err := s.SafeToString(1)
if err != nil {
return s.PushError("file_read: argument must be a string")
}
data, err := os.ReadFile(path)
if err != nil {
s.PushNil()
s.PushString(err.Error())
return 2
}
s.PushString(string(data))
return 1
},
"file_write": func(s *luajit.State) int {
if err := s.CheckExactArgs(2); err != nil {
return s.PushError("file_write: %v", err)
}
path, err := s.SafeToString(1)
if err != nil {
return s.PushError("file_write: first argument must be a string")
}
content, err := s.SafeToString(2)
if err != nil {
return s.PushError("file_write: second argument must be a string")
}
err = os.WriteFile(path, []byte(content), 0644)
if err != nil {
s.PushBoolean(false)
s.PushString(err.Error())
return 2
}
s.PushBoolean(true)
return 1
},
"file_append": func(s *luajit.State) int {
if err := s.CheckExactArgs(2); err != nil {
return s.PushError("file_append: %v", err)
}
path, err := s.SafeToString(1)
if err != nil {
return s.PushError("file_append: first argument must be a string")
}
content, err := s.SafeToString(2)
if err != nil {
return s.PushError("file_append: second argument must be a string")
}
file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
s.PushBoolean(false)
s.PushString(err.Error())
return 2
}
defer file.Close()
_, err = file.WriteString(content)
if err != nil {
s.PushBoolean(false)
s.PushString(err.Error())
return 2
}
s.PushBoolean(true)
return 1
},
"file_copy": func(s *luajit.State) int {
if err := s.CheckExactArgs(2); err != nil {
return s.PushError("file_copy: %v", err)
}
src, err := s.SafeToString(1)
if err != nil {
return s.PushError("file_copy: first argument must be a string")
}
dst, err := s.SafeToString(2)
if err != nil {
return s.PushError("file_copy: second argument must be a string")
}
srcFile, err := os.Open(src)
if err != nil {
s.PushBoolean(false)
s.PushString(err.Error())
return 2
}
defer srcFile.Close()
dstFile, err := os.Create(dst)
if err != nil {
s.PushBoolean(false)
s.PushString(err.Error())
return 2
}
defer dstFile.Close()
_, err = io.Copy(dstFile, srcFile)
if err != nil {
s.PushBoolean(false)
s.PushString(err.Error())
return 2
}
s.PushBoolean(true)
return 1
},
"file_move": func(s *luajit.State) int {
if err := s.CheckExactArgs(2); err != nil {
return s.PushError("file_move: %v", err)
}
src, err := s.SafeToString(1)
if err != nil {
return s.PushError("file_move: first argument must be a string")
}
dst, err := s.SafeToString(2)
if err != nil {
return s.PushError("file_move: second argument must be a string")
}
err = os.Rename(src, dst)
if err != nil {
s.PushBoolean(false)
s.PushString(err.Error())
return 2
}
s.PushBoolean(true)
return 1
},
"file_delete": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("file_delete: %v", err)
}
path, err := s.SafeToString(1)
if err != nil {
return s.PushError("file_delete: argument must be a string")
}
err = os.Remove(path)
if err != nil {
s.PushBoolean(false)
s.PushString(err.Error())
return 2
}
s.PushBoolean(true)
return 1
},
"file_mtime": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("file_mtime: %v", err)
}
path, err := s.SafeToString(1)
if err != nil {
return s.PushError("file_mtime: argument must be a string")
}
info, err := os.Stat(path)
if err != nil {
s.PushNumber(-1)
return 1
}
s.PushNumber(float64(info.ModTime().Unix()))
return 1
},
"dir_create": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("dir_create: %v", err)
}
path, err := s.SafeToString(1)
if err != nil {
return s.PushError("dir_create: argument must be a string")
}
err = os.MkdirAll(path, 0755)
if err != nil {
s.PushBoolean(false)
s.PushString(err.Error())
return 2
}
s.PushBoolean(true)
return 1
},
"dir_remove": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("dir_remove: %v", err)
}
path, err := s.SafeToString(1)
if err != nil {
return s.PushError("dir_remove: argument must be a string")
}
err = os.RemoveAll(path)
if err != nil {
s.PushBoolean(false)
s.PushString(err.Error())
return 2
}
s.PushBoolean(true)
return 1
},
"dir_list": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("dir_list: %v", err)
}
path, err := s.SafeToString(1)
if err != nil {
return s.PushError("dir_list: argument must be a string")
}
entries, err := os.ReadDir(path)
if err != nil {
s.PushNil()
s.PushString(err.Error())
return 2
}
s.CreateTable(len(entries), 0)
for i, entry := range entries {
s.PushNumber(float64(i + 1))
s.CreateTable(0, 4)
s.PushString("name")
s.PushString(entry.Name())
s.SetTable(-3)
s.PushString("is_dir")
s.PushBoolean(entry.IsDir())
s.SetTable(-3)
if info, err := entry.Info(); err == nil {
s.PushString("size")
s.PushNumber(float64(info.Size()))
s.SetTable(-3)
s.PushString("mtime")
s.PushNumber(float64(info.ModTime().Unix()))
s.SetTable(-3)
}
s.SetTable(-3)
}
return 1
},
"path_join": func(s *luajit.State) int {
var parts []string
for i := 1; i <= s.GetTop(); i++ {
part, err := s.SafeToString(i)
if err != nil {
return s.PushError("path_join: argument %d must be a string", i)
}
parts = append(parts, part)
}
s.PushString(filepath.Join(parts...))
return 1
},
"path_dir": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("path_dir: %v", err)
}
path, err := s.SafeToString(1)
if err != nil {
return s.PushError("path_dir: argument must be a string")
}
s.PushString(filepath.Dir(path))
return 1
},
"path_basename": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("path_basename: %v", err)
}
path, err := s.SafeToString(1)
if err != nil {
return s.PushError("path_basename: argument must be a string")
}
s.PushString(filepath.Base(path))
return 1
},
"path_ext": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("path_ext: %v", err)
}
path, err := s.SafeToString(1)
if err != nil {
return s.PushError("path_ext: argument must be a string")
}
s.PushString(filepath.Ext(path))
return 1
},
"path_abs": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("path_abs: %v", err)
}
path, err := s.SafeToString(1)
if err != nil {
return s.PushError("path_abs: argument must be a string")
}
abs, err := filepath.Abs(path)
if err != nil {
s.PushNil()
s.PushString(err.Error())
return 2
}
s.PushString(abs)
return 1
},
"path_clean": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("path_clean: %v", err)
}
path, err := s.SafeToString(1)
if err != nil {
return s.PushError("path_clean: argument must be a string")
}
s.PushString(filepath.Clean(path))
return 1
},
"path_split": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("path_split: %v", err)
}
path, err := s.SafeToString(1)
if err != nil {
return s.PushError("path_split: argument must be a string")
}
dir, file := filepath.Split(path)
s.PushString(dir)
s.PushString(file)
return 2
},
"temp_file": func(s *luajit.State) int {
var prefix string
if s.GetTop() >= 1 {
if p, err := s.SafeToString(1); err == nil {
prefix = p
}
}
file, err := os.CreateTemp("", prefix)
if err != nil {
s.PushNil()
s.PushString(err.Error())
return 2
}
defer file.Close()
s.PushString(file.Name())
return 1
},
"temp_dir": func(s *luajit.State) int {
var prefix string
if s.GetTop() >= 1 {
if p, err := s.SafeToString(1); err == nil {
prefix = p
}
}
dir, err := os.MkdirTemp("", prefix)
if err != nil {
s.PushNil()
s.PushString(err.Error())
return 2
}
s.PushString(dir)
return 1
},
"glob": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("glob: %v", err)
}
pattern, err := s.SafeToString(1)
if err != nil {
return s.PushError("glob: argument must be a string")
}
matches, err := filepath.Glob(pattern)
if err != nil {
s.PushNil()
s.PushString(err.Error())
return 2
}
if err := s.PushValue(matches); err != nil {
return s.PushError("glob: failed to push result: %v", err)
}
return 1
},
"walk": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("walk: %v", err)
}
root, err := s.SafeToString(1)
if err != nil {
return s.PushError("walk: argument must be a string")
}
var files []string
err = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return nil // Skip errors
}
files = append(files, path)
return nil
})
if err != nil {
s.PushNil()
s.PushString(err.Error())
return 2
}
if err := s.PushValue(files); err != nil {
return s.PushError("walk: failed to push result: %v", err)
}
return 1
},
"getcwd": func(s *luajit.State) int {
cwd, err := os.Getwd()
if err != nil {
s.PushNil()
s.PushString(err.Error())
return 2
}
s.PushString(cwd)
return 1
},
"chdir": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("chdir: %v", err)
}
path, err := s.SafeToString(1)
if err != nil {
return s.PushError("chdir: argument must be a string")
}
err = os.Chdir(path)
if err != nil {
s.PushBoolean(false)
s.PushString(err.Error())
return 2
}
s.PushBoolean(true)
return 1
},
"file_lines": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("file_lines: %v", err)
}
path, err := s.SafeToString(1)
if err != nil {
return s.PushError("file_lines: argument must be a string")
}
data, err := os.ReadFile(path)
if err != nil {
s.PushNil()
s.PushString(err.Error())
return 2
}
lines := strings.Split(string(data), "\n")
// Remove empty last line if file ends with newline
if len(lines) > 0 && lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
}
if err := s.PushValue(lines); err != nil {
return s.PushError("file_lines: failed to push result: %v", err)
}
return 1
},
"touch": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("touch: %v", err)
}
path, err := s.SafeToString(1)
if err != nil {
return s.PushError("touch: argument must be a string")
}
now := time.Now()
err = os.Chtimes(path, now, now)
if os.IsNotExist(err) {
// Create file if it doesn't exist
file, err := os.Create(path)
if err != nil {
s.PushBoolean(false)
s.PushString(err.Error())
return 2
}
file.Close()
} else if err != nil {
s.PushBoolean(false)
s.PushString(err.Error())
return 2
}
s.PushBoolean(true)
return 1
},
} }
} }

View File

@ -1,6 +1,10 @@
package functions package functions
import luajit "git.sharkk.net/Sky/LuaJIT-to-Go" import (
"maps"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
)
// Registry holds all available Go functions for Lua modules // Registry holds all available Go functions for Lua modules
type Registry map[string]luajit.GoFunction type Registry map[string]luajit.GoFunction
@ -9,26 +13,11 @@ type Registry map[string]luajit.GoFunction
func GetAll() Registry { func GetAll() Registry {
registry := make(Registry) registry := make(Registry)
// Register function groups maps.Copy(registry, GetJSONFunctions())
for name, fn := range GetJSONFunctions() { maps.Copy(registry, GetStringFunctions())
registry[name] = fn maps.Copy(registry, GetMathFunctions())
} maps.Copy(registry, GetFSFunctions())
maps.Copy(registry, GetCryptoFunctions())
for name, fn := range GetStringFunctions() {
registry[name] = fn
}
for name, fn := range GetMathFunctions() {
registry[name] = fn
}
for name, fn := range GetFSFunctions() {
registry[name] = fn
}
for name, fn := range GetCryptoFunctions() {
registry[name] = fn
}
return registry return registry
} }

View File

@ -1,36 +1,38 @@
// functions/string.go
package functions package functions
import ( import (
"fmt" "fmt"
"math/rand"
"regexp"
"strconv"
"strings" "strings"
"time"
"unicode"
"unicode/utf8"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go" luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
"golang.org/x/text/cases"
"golang.org/x/text/language"
) )
// GetStringFunctions returns all string manipulation Go functions
func GetStringFunctions() map[string]luajit.GoFunction { func GetStringFunctions() map[string]luajit.GoFunction {
return map[string]luajit.GoFunction{ return map[string]luajit.GoFunction{
"string_split": func(s *luajit.State) int { "string_split": func(s *luajit.State) int {
if err := s.CheckExactArgs(2); err != nil { if err := s.CheckExactArgs(2); err != nil {
return s.PushError("string_split: %v", err) return s.PushError("string_split: %v", err)
} }
str, err := s.SafeToString(1) str, err := s.SafeToString(1)
if err != nil { if err != nil {
return s.PushError("string_split: first argument must be a string") return s.PushError("string_split: first argument must be a string")
} }
sep, err := s.SafeToString(2) sep, err := s.SafeToString(2)
if err != nil { if err != nil {
return s.PushError("string_split: second argument must be a string") return s.PushError("string_split: second argument must be a string")
} }
parts := strings.Split(str, sep) parts := strings.Split(str, sep)
if err := s.PushValue(parts); err != nil { if err := s.PushValue(parts); err != nil {
return s.PushError("string_split: failed to push result: %v", err) return s.PushError("string_split: failed to push result: %v", err)
} }
return 1 return 1
}, },
@ -38,12 +40,10 @@ func GetStringFunctions() map[string]luajit.GoFunction {
if err := s.CheckExactArgs(2); err != nil { if err := s.CheckExactArgs(2); err != nil {
return s.PushError("string_join: %v", err) return s.PushError("string_join: %v", err)
} }
arr, err := s.SafeToTable(1) arr, err := s.SafeToTable(1)
if err != nil { if err != nil {
return s.PushError("string_join: first argument must be a table") return s.PushError("string_join: first argument must be a table")
} }
sep, err := s.SafeToString(2) sep, err := s.SafeToString(2)
if err != nil { if err != nil {
return s.PushError("string_join: second argument must be a string") return s.PushError("string_join: second argument must be a string")
@ -57,6 +57,13 @@ func GetStringFunctions() map[string]luajit.GoFunction {
for i, v := range anySlice { for i, v := range anySlice {
parts[i] = fmt.Sprintf("%v", v) parts[i] = fmt.Sprintf("%v", v)
} }
} else if anyMap, ok := arr.(map[string]interface{}); ok {
// Handle empty table case - check if it's meant to be an array
if len(anyMap) == 0 {
parts = []string{} // Empty array
} else {
return s.PushError("string_join: first argument must be an array")
}
} else { } else {
return s.PushError("string_join: first argument must be an array") return s.PushError("string_join: first argument must be an array")
} }
@ -70,26 +77,64 @@ func GetStringFunctions() map[string]luajit.GoFunction {
if err := s.CheckMinArgs(1); err != nil { if err := s.CheckMinArgs(1); err != nil {
return s.PushError("string_trim: %v", err) return s.PushError("string_trim: %v", err)
} }
str, err := s.SafeToString(1) str, err := s.SafeToString(1)
if err != nil { if err != nil {
return s.PushError("string_trim: argument must be a string") return s.PushError("string_trim: argument must be a string")
} }
s.PushString(strings.TrimSpace(str)) s.PushString(strings.TrimSpace(str))
return 1 return 1
}, },
"string_trim_left": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("string_trim_left: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_trim_left: first argument must be a string")
}
if s.GetTop() >= 2 && s.IsString(2) {
cutset, err := s.SafeToString(2)
if err != nil {
return s.PushError("string_trim_left: second argument must be a string")
}
s.PushString(strings.TrimLeft(str, cutset))
} else {
s.PushString(strings.TrimLeftFunc(str, unicode.IsSpace))
}
return 1
},
"string_trim_right": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("string_trim_right: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_trim_right: first argument must be a string")
}
if s.GetTop() >= 2 && s.IsString(2) {
cutset, err := s.SafeToString(2)
if err != nil {
return s.PushError("string_trim_right: second argument must be a string")
}
s.PushString(strings.TrimRight(str, cutset))
} else {
s.PushString(strings.TrimRightFunc(str, unicode.IsSpace))
}
return 1
},
"string_upper": func(s *luajit.State) int { "string_upper": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil { if err := s.CheckMinArgs(1); err != nil {
return s.PushError("string_upper: %v", err) return s.PushError("string_upper: %v", err)
} }
str, err := s.SafeToString(1) str, err := s.SafeToString(1)
if err != nil { if err != nil {
return s.PushError("string_upper: argument must be a string") return s.PushError("string_upper: argument must be a string")
} }
s.PushString(strings.ToUpper(str)) s.PushString(strings.ToUpper(str))
return 1 return 1
}, },
@ -98,58 +143,605 @@ func GetStringFunctions() map[string]luajit.GoFunction {
if err := s.CheckMinArgs(1); err != nil { if err := s.CheckMinArgs(1); err != nil {
return s.PushError("string_lower: %v", err) return s.PushError("string_lower: %v", err)
} }
str, err := s.SafeToString(1) str, err := s.SafeToString(1)
if err != nil { if err != nil {
return s.PushError("string_lower: argument must be a string") return s.PushError("string_lower: argument must be a string")
} }
s.PushString(strings.ToLower(str)) s.PushString(strings.ToLower(str))
return 1 return 1
}, },
"string_title": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("string_title: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_title: argument must be a string")
}
caser := cases.Title(language.English)
s.PushString(caser.String(str))
return 1
},
"string_contains": func(s *luajit.State) int { "string_contains": func(s *luajit.State) int {
if err := s.CheckExactArgs(2); err != nil { if err := s.CheckExactArgs(2); err != nil {
return s.PushError("string_contains: %v", err) return s.PushError("string_contains: %v", err)
} }
str, err := s.SafeToString(1) str, err := s.SafeToString(1)
if err != nil { if err != nil {
return s.PushError("string_contains: first argument must be a string") return s.PushError("string_contains: first argument must be a string")
} }
substr, err := s.SafeToString(2) substr, err := s.SafeToString(2)
if err != nil { if err != nil {
return s.PushError("string_contains: second argument must be a string") return s.PushError("string_contains: second argument must be a string")
} }
s.PushBoolean(strings.Contains(str, substr)) s.PushBoolean(strings.Contains(str, substr))
return 1 return 1
}, },
"string_starts_with": func(s *luajit.State) int {
if err := s.CheckExactArgs(2); err != nil {
return s.PushError("string_starts_with: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_starts_with: first argument must be a string")
}
prefix, err := s.SafeToString(2)
if err != nil {
return s.PushError("string_starts_with: second argument must be a string")
}
s.PushBoolean(strings.HasPrefix(str, prefix))
return 1
},
"string_ends_with": func(s *luajit.State) int {
if err := s.CheckExactArgs(2); err != nil {
return s.PushError("string_ends_with: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_ends_with: first argument must be a string")
}
suffix, err := s.SafeToString(2)
if err != nil {
return s.PushError("string_ends_with: second argument must be a string")
}
s.PushBoolean(strings.HasSuffix(str, suffix))
return 1
},
"string_replace": func(s *luajit.State) int { "string_replace": func(s *luajit.State) int {
if err := s.CheckExactArgs(3); err != nil { if err := s.CheckExactArgs(3); err != nil {
return s.PushError("string_replace: %v", err) return s.PushError("string_replace: %v", err)
} }
str, err := s.SafeToString(1) str, err := s.SafeToString(1)
if err != nil { if err != nil {
return s.PushError("string_replace: first argument must be a string") return s.PushError("string_replace: first argument must be a string")
} }
old, err := s.SafeToString(2) old, err := s.SafeToString(2)
if err != nil { if err != nil {
return s.PushError("string_replace: second argument must be a string") return s.PushError("string_replace: second argument must be a string")
} }
new, err := s.SafeToString(3) new, err := s.SafeToString(3)
if err != nil { if err != nil {
return s.PushError("string_replace: third argument must be a string") return s.PushError("string_replace: third argument must be a string")
} }
result := strings.ReplaceAll(str, old, new) result := strings.ReplaceAll(str, old, new)
s.PushString(result) s.PushString(result)
return 1 return 1
}, },
"string_replace_n": func(s *luajit.State) int {
if err := s.CheckExactArgs(4); err != nil {
return s.PushError("string_replace_n: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_replace_n: first argument must be a string")
}
old, err := s.SafeToString(2)
if err != nil {
return s.PushError("string_replace_n: second argument must be a string")
}
new, err := s.SafeToString(3)
if err != nil {
return s.PushError("string_replace_n: third argument must be a string")
}
n, err := s.SafeToNumber(4)
if err != nil || n != float64(int(n)) {
return s.PushError("string_replace_n: fourth argument must be an integer")
}
result := strings.Replace(str, old, new, int(n))
s.PushString(result)
return 1
},
"string_index": func(s *luajit.State) int {
if err := s.CheckExactArgs(2); err != nil {
return s.PushError("string_index: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_index: first argument must be a string")
}
substr, err := s.SafeToString(2)
if err != nil {
return s.PushError("string_index: second argument must be a string")
}
index := strings.Index(str, substr)
s.PushNumber(float64(index + 1)) // Lua is 1-indexed
return 1
},
"string_last_index": func(s *luajit.State) int {
if err := s.CheckExactArgs(2); err != nil {
return s.PushError("string_last_index: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_last_index: first argument must be a string")
}
substr, err := s.SafeToString(2)
if err != nil {
return s.PushError("string_last_index: second argument must be a string")
}
index := strings.LastIndex(str, substr)
if index == -1 {
s.PushNumber(0)
} else {
s.PushNumber(float64(index + 1)) // Lua is 1-indexed
}
return 1
},
"string_count": func(s *luajit.State) int {
if err := s.CheckExactArgs(2); err != nil {
return s.PushError("string_count: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_count: first argument must be a string")
}
substr, err := s.SafeToString(2)
if err != nil {
return s.PushError("string_count: second argument must be a string")
}
count := strings.Count(str, substr)
s.PushNumber(float64(count))
return 1
},
"string_repeat": func(s *luajit.State) int {
if err := s.CheckExactArgs(2); err != nil {
return s.PushError("string_repeat: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_repeat: first argument must be a string")
}
count, err := s.SafeToNumber(2)
if err != nil || count < 0 || count != float64(int(count)) {
return s.PushError("string_repeat: second argument must be a non-negative integer")
}
if count > 1000000 {
return s.PushError("string_repeat: count too large (max 1000000)")
}
result := strings.Repeat(str, int(count))
s.PushString(result)
return 1
},
"string_reverse": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("string_reverse: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_reverse: argument must be a string")
}
runes := []rune(str)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
s.PushString(string(runes))
return 1
},
"string_length": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("string_length: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_length: argument must be a string")
}
s.PushNumber(float64(utf8.RuneCountInString(str)))
return 1
},
"string_byte_length": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("string_byte_length: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_byte_length: argument must be a string")
}
s.PushNumber(float64(len(str)))
return 1
},
"string_lines": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("string_lines: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_lines: argument must be a string")
}
lines := strings.Split(str, "\n")
if err := s.PushValue(lines); err != nil {
return s.PushError("string_lines: failed to push result: %v", err)
}
return 1
},
"string_words": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("string_words: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_words: argument must be a string")
}
words := strings.Fields(str)
if err := s.PushValue(words); err != nil {
return s.PushError("string_words: failed to push result: %v", err)
}
return 1
},
"string_pad_left": func(s *luajit.State) int {
if err := s.CheckMinArgs(2); err != nil {
return s.PushError("string_pad_left: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_pad_left: first argument must be a string")
}
width, err := s.SafeToNumber(2)
if err != nil || width != float64(int(width)) {
return s.PushError("string_pad_left: second argument must be an integer")
}
padChar := " "
if s.GetTop() >= 3 {
if p, err := s.SafeToString(3); err == nil && len(p) > 0 {
padChar = string([]rune(p)[0])
}
}
currentLen := utf8.RuneCountInString(str)
targetLen := int(width)
if currentLen >= targetLen {
s.PushString(str)
return 1
}
padding := strings.Repeat(padChar, targetLen-currentLen)
s.PushString(padding + str)
return 1
},
"string_pad_right": func(s *luajit.State) int {
if err := s.CheckMinArgs(2); err != nil {
return s.PushError("string_pad_right: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_pad_right: first argument must be a string")
}
width, err := s.SafeToNumber(2)
if err != nil || width != float64(int(width)) {
return s.PushError("string_pad_right: second argument must be an integer")
}
padChar := " "
if s.GetTop() >= 3 {
if p, err := s.SafeToString(3); err == nil && len(p) > 0 {
padChar = string([]rune(p)[0])
}
}
currentLen := utf8.RuneCountInString(str)
targetLen := int(width)
if currentLen >= targetLen {
s.PushString(str)
return 1
}
padding := strings.Repeat(padChar, targetLen-currentLen)
s.PushString(str + padding)
return 1
},
"string_slice": func(s *luajit.State) int {
if err := s.CheckMinArgs(2); err != nil {
return s.PushError("string_slice: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_slice: first argument must be a string")
}
start, err := s.SafeToNumber(2)
if err != nil || start != float64(int(start)) {
return s.PushError("string_slice: second argument must be an integer")
}
runes := []rune(str)
length := len(runes)
startIdx := int(start) - 1 // Convert from 1-indexed to 0-indexed
if startIdx < 0 {
startIdx = 0
}
if startIdx >= length {
s.PushString("")
return 1
}
endIdx := length
if s.GetTop() >= 3 {
end, err := s.SafeToNumber(3)
if err == nil && end == float64(int(end)) {
endIdx = int(end)
if endIdx < 0 {
endIdx = 0
}
if endIdx > length {
endIdx = length
}
}
}
if startIdx >= endIdx {
s.PushString("")
return 1
}
s.PushString(string(runes[startIdx:endIdx]))
return 1
},
"regex_match": func(s *luajit.State) int {
if err := s.CheckExactArgs(2); err != nil {
return s.PushError("regex_match: %v", err)
}
pattern, err := s.SafeToString(1)
if err != nil {
return s.PushError("regex_match: first argument must be a string")
}
str, err := s.SafeToString(2)
if err != nil {
return s.PushError("regex_match: second argument must be a string")
}
re, err := regexp.Compile(pattern)
if err != nil {
s.PushBoolean(false)
s.PushString(err.Error())
return 2
}
s.PushBoolean(re.MatchString(str))
return 1
},
"regex_find": func(s *luajit.State) int {
if err := s.CheckExactArgs(2); err != nil {
return s.PushError("regex_find: %v", err)
}
pattern, err := s.SafeToString(1)
if err != nil {
return s.PushError("regex_find: first argument must be a string")
}
str, err := s.SafeToString(2)
if err != nil {
return s.PushError("regex_find: second argument must be a string")
}
re, err := regexp.Compile(pattern)
if err != nil {
s.PushNil()
s.PushString(err.Error())
return 2
}
match := re.FindString(str)
if match == "" {
s.PushNil()
} else {
s.PushString(match)
}
return 1
},
"regex_find_all": func(s *luajit.State) int {
if err := s.CheckExactArgs(2); err != nil {
return s.PushError("regex_find_all: %v", err)
}
pattern, err := s.SafeToString(1)
if err != nil {
return s.PushError("regex_find_all: first argument must be a string")
}
str, err := s.SafeToString(2)
if err != nil {
return s.PushError("regex_find_all: second argument must be a string")
}
re, err := regexp.Compile(pattern)
if err != nil {
s.PushNil()
s.PushString(err.Error())
return 2
}
matches := re.FindAllString(str, -1)
if err := s.PushValue(matches); err != nil {
return s.PushError("regex_find_all: failed to push result: %v", err)
}
return 1
},
"regex_replace": func(s *luajit.State) int {
if err := s.CheckExactArgs(3); err != nil {
return s.PushError("regex_replace: %v", err)
}
pattern, err := s.SafeToString(1)
if err != nil {
return s.PushError("regex_replace: first argument must be a string")
}
str, err := s.SafeToString(2)
if err != nil {
return s.PushError("regex_replace: second argument must be a string")
}
replacement, err := s.SafeToString(3)
if err != nil {
return s.PushError("regex_replace: third argument must be a string")
}
re, err := regexp.Compile(pattern)
if err != nil {
s.PushNil()
s.PushString(err.Error())
return 2
}
result := re.ReplaceAllString(str, replacement)
s.PushString(result)
return 1
},
"string_to_number": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("string_to_number: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_to_number: argument must be a string")
}
if num, err := strconv.ParseFloat(str, 64); err == nil {
s.PushNumber(num)
return 1
}
if num, err := strconv.ParseInt(str, 10, 64); err == nil {
s.PushNumber(float64(num))
return 1
}
s.PushNil()
return 1
},
"string_is_numeric": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("string_is_numeric: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_is_numeric: argument must be a string")
}
_, err1 := strconv.ParseFloat(str, 64)
_, err2 := strconv.ParseInt(str, 10, 64)
s.PushBoolean(err1 == nil || err2 == nil)
return 1
},
"string_is_alpha": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("string_is_alpha: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_is_alpha: argument must be a string")
}
if len(str) == 0 {
s.PushBoolean(false)
return 1
}
for _, r := range str {
if !unicode.IsLetter(r) {
s.PushBoolean(false)
return 1
}
}
s.PushBoolean(true)
return 1
},
"string_is_alphanumeric": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("string_is_alphanumeric: %v", err)
}
str, err := s.SafeToString(1)
if err != nil {
return s.PushError("string_is_alphanumeric: argument must be a string")
}
if len(str) == 0 {
s.PushBoolean(false)
return 1
}
for _, r := range str {
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
s.PushBoolean(false)
return 1
}
}
s.PushBoolean(true)
return 1
},
"random_string": func(s *luajit.State) int {
if err := s.CheckMinArgs(1); err != nil {
return s.PushError("random_string: %v", err)
}
length, err := s.SafeToNumber(1)
if err != nil || length != float64(int(length)) || length < 0 {
return s.PushError("random_string: first argument must be a non-negative integer")
}
charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
if s.GetTop() >= 2 {
if custom, err := s.SafeToString(2); err == nil && len(custom) > 0 {
charset = custom
}
}
n := int(length)
if n == 0 {
s.PushString("")
return 1
}
if n > 100000 {
return s.PushError("random_string: length too large (max 100000)")
}
result := make([]byte, n)
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := range result {
result[i] = charset[rnd.Intn(len(charset))]
}
s.PushString(string(result))
return 1
},
} }
} }

5
go.mod
View File

@ -3,4 +3,9 @@ module Moonshark
go 1.24.1 go 1.24.1
require git.sharkk.net/Sky/LuaJIT-to-Go v0.5.6 require git.sharkk.net/Sky/LuaJIT-to-Go v0.5.6
require github.com/goccy/go-json v0.10.5 require github.com/goccy/go-json v0.10.5
require github.com/google/uuid v1.6.0
require golang.org/x/text v0.27.0

4
go.sum
View File

@ -2,3 +2,7 @@ git.sharkk.net/Sky/LuaJIT-to-Go v0.5.6 h1:XytP9R2fWykv0MXIzxggPx5S/PmTkjyZVvUX2s
git.sharkk.net/Sky/LuaJIT-to-Go v0.5.6/go.mod h1:HQz+D7AFxOfNbTIogjxP+shEBtz1KKrLlLucU+w07c8= git.sharkk.net/Sky/LuaJIT-to-Go v0.5.6/go.mod h1:HQz+D7AFxOfNbTIogjxP+shEBtz1KKrLlLucU+w07c8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=

371
modules/crypto.lua Normal file
View File

@ -0,0 +1,371 @@
-- modules/crypto.lua - Comprehensive cryptographic utilities
local crypto = {}
-- ======================================================================
-- ENCODING / DECODING
-- ======================================================================
function crypto.base64_encode(data)
local result, err = moonshark.base64_encode(data)
if not result then
error(err)
end
return result
end
function crypto.base64_decode(data)
local result, err = moonshark.base64_decode(data)
if not result then
error(err)
end
return result
end
function crypto.base64_url_encode(data)
local result, err = moonshark.base64_url_encode(data)
if not result then
error(err)
end
return result
end
function crypto.base64_url_decode(data)
local result, err = moonshark.base64_url_decode(data)
if not result then
error(err)
end
return result
end
function crypto.hex_encode(data)
local result, err = moonshark.hex_encode(data)
if not result then
error(err)
end
return result
end
function crypto.hex_decode(data)
local result, err = moonshark.hex_decode(data)
if not result then
error(err)
end
return result
end
-- ======================================================================
-- HASHING FUNCTIONS
-- ======================================================================
function crypto.md5(data)
local result, err = moonshark.md5_hash(data)
if not result then
error(err)
end
return result
end
function crypto.sha1(data)
local result, err = moonshark.sha1_hash(data)
if not result then
error(err)
end
return result
end
function crypto.sha256(data)
local result, err = moonshark.sha256_hash(data)
if not result then
error(err)
end
return result
end
function crypto.sha512(data)
local result, err = moonshark.sha512_hash(data)
if not result then
error(err)
end
return result
end
-- Hash file contents
function crypto.hash_file(path, algorithm)
algorithm = algorithm or "sha256"
if not moonshark.file_exists(path) then
error("File not found: " .. path)
end
local content = moonshark.file_read(path)
if not content then
error("Failed to read file: " .. path)
end
if algorithm == "md5" then
return crypto.md5(content)
elseif algorithm == "sha1" then
return crypto.sha1(content)
elseif algorithm == "sha256" then
return crypto.sha256(content)
elseif algorithm == "sha512" then
return crypto.sha512(content)
else
error("Unsupported hash algorithm: " .. algorithm)
end
end
-- ======================================================================
-- HMAC FUNCTIONS
-- ======================================================================
function crypto.hmac_sha1(message, key)
local result, err = moonshark.hmac_sha1(message, key)
if not result then
error(err)
end
return result
end
function crypto.hmac_sha256(message, key)
local result, err = moonshark.hmac_sha256(message, key)
if not result then
error(err)
end
return result
end
-- ======================================================================
-- UUID FUNCTIONS
-- ======================================================================
function crypto.uuid()
return moonshark.uuid_generate()
end
function crypto.uuid_v4()
return moonshark.uuid_generate_v4()
end
function crypto.is_uuid(str)
return moonshark.uuid_validate(str)
end
-- ======================================================================
-- RANDOM GENERATORS
-- ======================================================================
function crypto.random_bytes(length)
local result, err = moonshark.random_bytes(length)
if not result then
error(err)
end
return result
end
function crypto.random_hex(length)
local result, err = moonshark.random_hex(length)
if not result then
error(err)
end
return result
end
function crypto.random_string(length, charset)
local result, err = moonshark.random_string(length, charset)
if not result then
error(err)
end
return result
end
-- Generate random alphanumeric string
function crypto.random_alphanumeric(length)
return crypto.random_string(length, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
end
-- Generate random password with mixed characters
function crypto.random_password(length, include_symbols)
length = length or 12
local charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
if include_symbols then
charset = charset .. "!@#$%^&*()_+-=[]{}|;:,.<>?"
end
return crypto.random_string(length, charset)
end
-- Generate cryptographically secure token
function crypto.token(length)
length = length or 32
return crypto.random_hex(length)
end
-- ======================================================================
-- UTILITY FUNCTIONS
-- ======================================================================
function crypto.secure_compare(a, b)
return moonshark.secure_compare(a, b)
end
-- Generate checksum for data integrity
function crypto.checksum(data, algorithm)
algorithm = algorithm or "sha256"
return crypto.hash_file and crypto[algorithm] and crypto[algorithm](data) or error("Invalid algorithm")
end
-- Verify data against checksum
function crypto.verify_checksum(data, expected, algorithm)
algorithm = algorithm or "sha256"
local actual = crypto[algorithm](data)
return crypto.secure_compare(actual, expected)
end
-- Simple encryption using XOR (not cryptographically secure)
function crypto.xor_encrypt(data, key)
local result = {}
local key_len = #key
for i = 1, #data do
local data_byte = string.byte(data, i)
local key_byte = string.byte(key, ((i - 1) % key_len) + 1)
table.insert(result, string.char(bit32 and bit32.bxor(data_byte, key_byte) or bit.bxor(data_byte, key_byte)))
end
return table.concat(result)
end
-- XOR decryption (same as encryption)
function crypto.xor_decrypt(data, key)
return crypto.xor_encrypt(data, key)
end
-- Generate hash chain for proof of work
function crypto.hash_chain(data, iterations, algorithm)
iterations = iterations or 1000
algorithm = algorithm or "sha256"
local result = data
for i = 1, iterations do
result = crypto[algorithm](result)
end
return result
end
-- Key derivation using PBKDF2-like approach (simplified)
function crypto.derive_key(password, salt, iterations, algorithm)
iterations = iterations or 10000
algorithm = algorithm or "sha256"
salt = salt or crypto.random_hex(16)
local derived = password .. salt
for i = 1, iterations do
derived = crypto[algorithm](derived)
end
return derived, salt
end
-- Generate nonce (number used once)
function crypto.nonce(length)
length = length or 16
return crypto.random_hex(length)
end
-- Create message authentication code
function crypto.mac(message, key, algorithm)
algorithm = algorithm or "sha256"
return crypto["hmac_" .. algorithm](message, key)
end
-- Verify message authentication code
function crypto.verify_mac(message, key, mac, algorithm)
algorithm = algorithm or "sha256"
local expected = crypto.mac(message, key, algorithm)
return crypto.secure_compare(expected, mac)
end
-- ======================================================================
-- CONVENIENCE FUNCTIONS
-- ======================================================================
-- One-shot encoding chain
function crypto.encode_chain(data, formats)
formats = formats or {"base64"}
local result = data
for _, format in ipairs(formats) do
if format == "base64" then
result = crypto.base64_encode(result)
elseif format == "base64url" then
result = crypto.base64_url_encode(result)
elseif format == "hex" then
result = crypto.hex_encode(result)
else
error("Unknown encoding format: " .. format)
end
end
return result
end
-- One-shot decoding chain (reverse order)
function crypto.decode_chain(data, formats)
formats = formats or {"base64"}
local result = data
-- Reverse the formats for decoding
for i = #formats, 1, -1 do
local format = formats[i]
if format == "base64" then
result = crypto.base64_decode(result)
elseif format == "base64url" then
result = crypto.base64_url_decode(result)
elseif format == "hex" then
result = crypto.hex_decode(result)
else
error("Unknown decoding format: " .. format)
end
end
return result
end
-- Hash multiple inputs
function crypto.hash_multiple(inputs, algorithm)
algorithm = algorithm or "sha256"
local combined = table.concat(inputs, "")
return crypto[algorithm](combined)
end
-- Create fingerprint from table data
function crypto.fingerprint(data, algorithm)
algorithm = algorithm or "sha256"
local json = moonshark.json_encode(data)
return crypto[algorithm](json)
end
-- Simple data integrity check
function crypto.integrity_check(data)
return {
data = data,
hash = crypto.sha256(data),
timestamp = os.time(),
uuid = crypto.uuid()
}
end
-- Verify integrity check
function crypto.verify_integrity(check)
if not check.data or not check.hash then
return false
end
local expected = crypto.sha256(check.data)
return crypto.secure_compare(expected, check.hash)
end
return crypto

446
modules/fs.lua Normal file
View File

@ -0,0 +1,446 @@
-- modules/fs.lua - Comprehensive filesystem utilities
local fs = {}
-- ======================================================================
-- FILE OPERATIONS
-- ======================================================================
function fs.exists(path)
return moonshark.file_exists(path)
end
function fs.size(path)
local size = moonshark.file_size(path)
return size >= 0 and size or nil
end
function fs.is_dir(path)
return moonshark.file_is_dir(path)
end
function fs.is_file(path)
return fs.exists(path) and not fs.is_dir(path)
end
function fs.read(path)
local content, err = moonshark.file_read(path)
if not content then
error("Failed to read file '" .. path .. "': " .. (err or "unknown error"))
end
return content
end
function fs.write(path, content)
local success, err = moonshark.file_write(path, content)
if not success then
error("Failed to write file '" .. path .. "': " .. (err or "unknown error"))
end
return true
end
function fs.append(path, content)
local success, err = moonshark.file_append(path, content)
if not success then
error("Failed to append to file '" .. path .. "': " .. (err or "unknown error"))
end
return true
end
function fs.copy(src, dst)
local success, err = moonshark.file_copy(src, dst)
if not success then
error("Failed to copy '" .. src .. "' to '" .. dst .. "': " .. (err or "unknown error"))
end
return true
end
function fs.move(src, dst)
local success, err = moonshark.file_move(src, dst)
if not success then
error("Failed to move '" .. src .. "' to '" .. dst .. "': " .. (err or "unknown error"))
end
return true
end
function fs.remove(path)
local success, err = moonshark.file_delete(path)
if not success then
error("Failed to remove '" .. path .. "': " .. (err or "unknown error"))
end
return true
end
function fs.mtime(path)
local timestamp = moonshark.file_mtime(path)
return timestamp >= 0 and timestamp or nil
end
function fs.touch(path)
local success, err = moonshark.touch(path)
if not success then
error("Failed to touch '" .. path .. "': " .. (err or "unknown error"))
end
return true
end
function fs.lines(path)
local lines, err = moonshark.file_lines(path)
if not lines then
error("Failed to read lines from '" .. path .. "': " .. (err or "unknown error"))
end
return lines
end
-- ======================================================================
-- DIRECTORY OPERATIONS
-- ======================================================================
function fs.mkdir(path)
local success, err = moonshark.dir_create(path)
if not success then
error("Failed to create directory '" .. path .. "': " .. (err or "unknown error"))
end
return true
end
function fs.rmdir(path)
local success, err = moonshark.dir_remove(path)
if not success then
error("Failed to remove directory '" .. path .. "': " .. (err or "unknown error"))
end
return true
end
function fs.list(path)
local entries, err = moonshark.dir_list(path)
if not entries then
error("Failed to list directory '" .. path .. "': " .. (err or "unknown error"))
end
return entries
end
function fs.list_files(path)
local entries = fs.list(path)
local files = {}
for _, entry in ipairs(entries) do
if not entry.is_dir then
table.insert(files, entry)
end
end
return files
end
function fs.list_dirs(path)
local entries = fs.list(path)
local dirs = {}
for _, entry in ipairs(entries) do
if entry.is_dir then
table.insert(dirs, entry)
end
end
return dirs
end
function fs.list_names(path)
local entries = fs.list(path)
local names = {}
for _, entry in ipairs(entries) do
table.insert(names, entry.name)
end
return names
end
-- ======================================================================
-- PATH OPERATIONS
-- ======================================================================
function fs.join(...)
return moonshark.path_join(...)
end
function fs.dirname(path)
return moonshark.path_dir(path)
end
function fs.basename(path)
return moonshark.path_basename(path)
end
function fs.ext(path)
return moonshark.path_ext(path)
end
function fs.abs(path)
local abs_path, err = moonshark.path_abs(path)
if not abs_path then
error("Failed to get absolute path for '" .. path .. "': " .. (err or "unknown error"))
end
return abs_path
end
function fs.clean(path)
return moonshark.path_clean(path)
end
function fs.split(path)
return moonshark.path_split(path)
end
-- Split path into directory, name, and extension
function fs.splitext(path)
local dir = fs.dirname(path)
local base = fs.basename(path)
local ext = fs.ext(path)
local name = base
if ext ~= "" then
name = base:sub(1, -(#ext + 1))
end
return dir, name, ext
end
-- ======================================================================
-- WORKING DIRECTORY
-- ======================================================================
function fs.getcwd()
local cwd, err = moonshark.getcwd()
if not cwd then
error("Failed to get current directory: " .. (err or "unknown error"))
end
return cwd
end
function fs.chdir(path)
local success, err = moonshark.chdir(path)
if not success then
error("Failed to change directory to '" .. path .. "': " .. (err or "unknown error"))
end
return true
end
-- ======================================================================
-- TEMPORARY FILES
-- ======================================================================
function fs.tempfile(prefix)
local path, err = moonshark.temp_file(prefix)
if not path then
error("Failed to create temporary file: " .. (err or "unknown error"))
end
return path
end
function fs.tempdir(prefix)
local path, err = moonshark.temp_dir(prefix)
if not path then
error("Failed to create temporary directory: " .. (err or "unknown error"))
end
return path
end
-- ======================================================================
-- PATTERN MATCHING
-- ======================================================================
function fs.glob(pattern)
local matches, err = moonshark.glob(pattern)
if not matches then
error("Failed to glob pattern '" .. pattern .. "': " .. (err or "unknown error"))
end
return matches
end
function fs.walk(root)
local files, err = moonshark.walk(root)
if not files then
error("Failed to walk directory '" .. root .. "': " .. (err or "unknown error"))
end
return files
end
-- ======================================================================
-- UTILITY FUNCTIONS
-- ======================================================================
-- Get file extension without dot
function fs.extension(path)
local ext = fs.ext(path)
return ext:sub(2) -- Remove leading dot
end
-- Change file extension
function fs.change_ext(path, new_ext)
local dir, name, _ = fs.splitext(path)
if not new_ext:match("^%.") then
new_ext = "." .. new_ext
end
return fs.join(dir, name .. new_ext)
end
-- Ensure directory exists
function fs.ensure_dir(path)
if not fs.exists(path) then
fs.mkdir(path)
elseif not fs.is_dir(path) then
error("Path exists but is not a directory: " .. path)
end
return true
end
-- Get file size in human readable format
function fs.size_human(path)
local size = fs.size(path)
if not size then return nil end
local units = {"B", "KB", "MB", "GB", "TB"}
local unit_index = 1
local size_float = tonumber(size)
while size_float >= 1024 and unit_index < #units do
size_float = size_float / 1024
unit_index = unit_index + 1
end
if unit_index == 1 then
return string.format("%d %s", size_float, units[unit_index])
else
return string.format("%.1f %s", size_float, units[unit_index])
end
end
-- Check if path is safe (doesn't contain .. or other dangerous patterns)
function fs.is_safe_path(path)
-- Normalize path
path = fs.clean(path)
-- Check for dangerous patterns
if path:match("%.%.") then return false end
if path:match("^/") then return false end -- Absolute paths might be dangerous
if path:match("^~") then return false end -- Home directory references
return true
end
-- Create directory tree
function fs.makedirs(path)
return fs.mkdir(path) -- mkdir already creates parent directories
end
-- Remove directory tree
function fs.removedirs(path)
return fs.rmdir(path) -- rmdir already removes recursively
end
-- Copy directory tree
function fs.copytree(src, dst)
if not fs.exists(src) then
error("Source directory does not exist: " .. src)
end
if not fs.is_dir(src) then
error("Source is not a directory: " .. src)
end
fs.mkdir(dst)
local entries = fs.list(src)
for _, entry in ipairs(entries) do
local src_path = fs.join(src, entry.name)
local dst_path = fs.join(dst, entry.name)
if entry.is_dir then
fs.copytree(src_path, dst_path)
else
fs.copy(src_path, dst_path)
end
end
return true
end
-- Find files by pattern
function fs.find(root, pattern, recursive)
recursive = recursive ~= false
local results = {}
local function search(dir)
local entries = fs.list(dir)
for _, entry in ipairs(entries) do
local full_path = fs.join(dir, entry.name)
if not entry.is_dir and entry.name:match(pattern) then
table.insert(results, full_path)
elseif entry.is_dir and recursive then
search(full_path)
end
end
end
search(root)
return results
end
-- Get directory tree as nested table
function fs.tree(root, max_depth)
max_depth = max_depth or 10
local function build_tree(path, depth)
if depth > max_depth then return nil end
if not fs.exists(path) then return nil end
local node = {
name = fs.basename(path),
path = path,
is_dir = fs.is_dir(path)
}
if node.is_dir then
node.children = {}
local entries = fs.list(path)
for _, entry in ipairs(entries) do
local child_path = fs.join(path, entry.name)
local child = build_tree(child_path, depth + 1)
if child then
table.insert(node.children, child)
end
end
else
node.size = fs.size(path)
node.mtime = fs.mtime(path)
end
return node
end
return build_tree(root, 1)
end
-- Monitor file changes (simplified version)
function fs.watch(path, callback, interval)
interval = interval or 1
if not fs.exists(path) then
error("Path does not exist: " .. path)
end
local last_mtime = fs.mtime(path)
while true do
local current_mtime = fs.mtime(path)
if current_mtime and current_mtime ~= last_mtime then
callback(path, "modified")
last_mtime = current_mtime
end
-- Sleep for interval (this would need a proper sleep function)
-- This is a placeholder - real implementation would need proper timing
local start = os.clock()
while os.clock() - start < interval do end
end
end
return fs

464
modules/string.lua Normal file
View File

@ -0,0 +1,464 @@
-- modules/string.lua - Comprehensive string manipulation utilities
local str = {}
-- ======================================================================
-- BASIC STRING OPERATIONS
-- ======================================================================
function str.split(s, delimiter)
return moonshark.string_split(s, delimiter)
end
function str.join(arr, separator)
return moonshark.string_join(arr, separator)
end
function str.trim(s)
return moonshark.string_trim(s)
end
function str.trim_left(s, cutset)
return moonshark.string_trim_left(s, cutset)
end
function str.trim_right(s, cutset)
return moonshark.string_trim_right(s, cutset)
end
function str.upper(s)
return moonshark.string_upper(s)
end
function str.lower(s)
return moonshark.string_lower(s)
end
function str.title(s)
return moonshark.string_title(s)
end
function str.contains(s, substr)
return moonshark.string_contains(s, substr)
end
function str.starts_with(s, prefix)
return moonshark.string_starts_with(s, prefix)
end
function str.ends_with(s, suffix)
return moonshark.string_ends_with(s, suffix)
end
function str.replace(s, old, new)
return moonshark.string_replace(s, old, new)
end
function str.replace_n(s, old, new, n)
return moonshark.string_replace_n(s, old, new, n)
end
function str.index(s, substr)
local idx = moonshark.string_index(s, substr)
return idx > 0 and idx or nil
end
function str.last_index(s, substr)
local idx = moonshark.string_last_index(s, substr)
return idx > 0 and idx or nil
end
function str.count(s, substr)
return moonshark.string_count(s, substr)
end
function str.repeat_(s, n)
return moonshark.string_repeat(s, n)
end
function str.reverse(s)
return moonshark.string_reverse(s)
end
function str.length(s)
return moonshark.string_length(s)
end
function str.byte_length(s)
return moonshark.string_byte_length(s)
end
function str.lines(s)
return moonshark.string_lines(s)
end
function str.words(s)
return moonshark.string_words(s)
end
function str.pad_left(s, width, pad_char)
return moonshark.string_pad_left(s, width, pad_char)
end
function str.pad_right(s, width, pad_char)
return moonshark.string_pad_right(s, width, pad_char)
end
function str.slice(s, start, end_pos)
return moonshark.string_slice(s, start, end_pos)
end
-- ======================================================================
-- REGULAR EXPRESSIONS
-- ======================================================================
function str.match(pattern, s)
return moonshark.regex_match(pattern, s)
end
function str.find(pattern, s)
return moonshark.regex_find(pattern, s)
end
function str.find_all(pattern, s)
return moonshark.regex_find_all(pattern, s)
end
function str.gsub(pattern, s, replacement)
return moonshark.regex_replace(pattern, s, replacement)
end
-- ======================================================================
-- TYPE CONVERSION & VALIDATION
-- ======================================================================
function str.to_number(s)
return moonshark.string_to_number(s)
end
function str.is_numeric(s)
return moonshark.string_is_numeric(s)
end
function str.is_alpha(s)
return moonshark.string_is_alpha(s)
end
function str.is_alphanumeric(s)
return moonshark.string_is_alphanumeric(s)
end
function str.is_empty(s)
return s == nil or s == ""
end
function str.is_blank(s)
return str.is_empty(s) or str.trim(s) == ""
end
-- ======================================================================
-- ADVANCED STRING OPERATIONS
-- ======================================================================
-- Capitalize first letter of each word
function str.capitalize(s)
return s:gsub("(%a)([%w_']*)", function(first, rest)
return str.upper(first) .. str.lower(rest)
end)
end
-- Convert string to camelCase
function str.camel_case(s)
local words = str.words(str.lower(s))
if #words == 0 then return s end
local result = words[1]
for i = 2, #words do
result = result .. str.capitalize(words[i])
end
return result
end
-- Convert string to PascalCase
function str.pascal_case(s)
local words = str.words(str.lower(s))
local result = ""
for _, word in ipairs(words) do
result = result .. str.capitalize(word)
end
return result
end
-- Convert string to snake_case
function str.snake_case(s)
local words = str.words(str.lower(s))
return str.join(words, "_")
end
-- Convert string to kebab-case
function str.kebab_case(s)
local words = str.words(str.lower(s))
return str.join(words, "-")
end
-- Convert string to SCREAMING_SNAKE_CASE
function str.screaming_snake_case(s)
return str.upper(str.snake_case(s))
end
-- Center text within given width
function str.center(s, width, fill_char)
fill_char = fill_char or " "
local len = str.length(s)
if len >= width then return s end
local pad_total = width - len
local pad_left = math.floor(pad_total / 2)
local pad_right = pad_total - pad_left
return string.rep(fill_char, pad_left) .. s .. string.rep(fill_char, pad_right)
end
-- Truncate string to maximum length
function str.truncate(s, max_length, suffix)
suffix = suffix or "..."
if str.length(s) <= max_length then
return s
end
local main_part = str.slice(s, 1, max_length - str.length(suffix))
main_part = str.trim_right(main_part)
return main_part .. suffix
end
-- Wrap text to specified width
function str.wrap(s, width)
local words = str.words(s)
local lines = {}
local current_line = ""
for _, word in ipairs(words) do
if str.length(current_line) + str.length(word) + 1 <= width then
if current_line == "" then
current_line = word
else
current_line = current_line .. " " .. word
end
else
if current_line ~= "" then
table.insert(lines, current_line)
end
current_line = word
end
end
if current_line ~= "" then
table.insert(lines, current_line)
end
return lines
end
-- Remove common leading whitespace
function str.dedent(s)
local lines = str.split(s, "\n")
if #lines <= 1 then return s end
-- Find minimum indentation (excluding empty lines)
local min_indent = math.huge
for _, line in ipairs(lines) do
if str.trim(line) ~= "" then
local indent = line:match("^%s*")
min_indent = math.min(min_indent, #indent)
end
end
if min_indent == math.huge or min_indent == 0 then
return s
end
-- Remove common indentation
for i, line in ipairs(lines) do
if str.trim(line) ~= "" then
lines[i] = line:sub(min_indent + 1)
end
end
return str.join(lines, "\n")
end
-- Escape special characters for regex
function str.escape_regex(s)
return s:gsub("([%.%+%*%?%[%]%^%$%(%)%{%}%|%\\])", "\\%1")
end
-- Quote string for shell usage
function str.shell_quote(s)
return "'" .. s:gsub("'", "'\"'\"'") .. "'"
end
-- URL encode string
function str.url_encode(s)
return s:gsub("([^%w%-%.%_%~])", function(c)
return string.format("%%%02X", string.byte(c))
end)
end
-- URL decode string
function str.url_decode(s)
return s:gsub("%%(%x%x)", function(hex)
return string.char(tonumber(hex, 16))
end):gsub("+", " ")
end
-- ======================================================================
-- STRING COMPARISON
-- ======================================================================
-- Case-insensitive comparison
function str.iequals(a, b)
return str.lower(a) == str.lower(b)
end
-- Levenshtein distance
function str.distance(a, b)
local len_a, len_b = str.length(a), str.length(b)
local matrix = {}
-- Initialize matrix
for i = 0, len_a do
matrix[i] = {[0] = i}
end
for j = 0, len_b do
matrix[0][j] = j
end
-- Fill matrix
for i = 1, len_a do
for j = 1, len_b do
local cost = (str.slice(a, i, i) == str.slice(b, j, j)) and 0 or 1
matrix[i][j] = math.min(
matrix[i-1][j] + 1, -- deletion
matrix[i][j-1] + 1, -- insertion
matrix[i-1][j-1] + cost -- substitution
)
end
end
return matrix[len_a][len_b]
end
-- String similarity (0-1)
function str.similarity(a, b)
local max_len = math.max(str.length(a), str.length(b))
if max_len == 0 then return 1 end
return 1 - (str.distance(a, b) / max_len)
end
-- ======================================================================
-- TEMPLATE FUNCTIONS
-- ======================================================================
-- Simple template substitution
function str.template(template, vars)
vars = vars or {}
return template:gsub("%${([%w_]+)}", function(var)
return tostring(vars[var] or "")
end)
end
-- Advanced template with functions
function str.template_advanced(template, context)
context = context or {}
return template:gsub("%${([^}]+)}", function(expr)
-- Simple variable substitution
if context[expr] then
return tostring(context[expr])
end
-- Handle simple expressions like ${var.prop}
local parts = str.split(expr, ".")
local value = context
for _, part in ipairs(parts) do
if type(value) == "table" and value[part] then
value = value[part]
else
return ""
end
end
return tostring(value)
end)
end
-- ======================================================================
-- UTILITY FUNCTIONS
-- ======================================================================
-- Check if string contains only whitespace
function str.is_whitespace(s)
return s:match("^%s*$") ~= nil
end
-- Remove all whitespace
function str.strip_whitespace(s)
return s:gsub("%s", "")
end
-- Normalize whitespace (replace multiple spaces with single space)
function str.normalize_whitespace(s)
return str.trim(s:gsub("%s+", " "))
end
-- Extract numbers from string
function str.extract_numbers(s)
local numbers = {}
for num in s:gmatch("%-?%d+%.?%d*") do
local n = tonumber(num)
if n then table.insert(numbers, n) end
end
return numbers
end
-- Remove diacritics/accents
function str.remove_accents(s)
local accents = {
["à"] = "a", ["á"] = "a", ["â"] = "a", ["ã"] = "a", ["ä"] = "a",
["è"] = "e", ["é"] = "e", ["ê"] = "e", ["ë"] = "e",
["ì"] = "i", ["í"] = "i", ["î"] = "i", ["ï"] = "i",
["ò"] = "o", ["ó"] = "o", ["ô"] = "o", ["õ"] = "o", ["ö"] = "o",
["ù"] = "u", ["ú"] = "u", ["û"] = "u", ["ü"] = "u",
["ñ"] = "n", ["ç"] = "c"
}
local result = s
for accented, plain in pairs(accents) do
result = result:gsub(accented, plain)
end
return result
end
-- Generate random string
function str.random(length, charset)
return moonshark.random_string(length, charset)
end
-- Check if string is valid UTF-8
function str.is_utf8(s)
-- Simple check - if we can iterate through the string as UTF-8, it's valid
local success = pcall(function()
for p, c in utf8 and utf8.codes or string.gmatch(s, ".") do
-- Just iterate through
end
end)
return success
end
-- Generate slug from string
function str.slug(s)
local kebab = str.kebab_case(str.remove_accents(s))
local cleaned = (kebab:gsub("[^%w%-]", ""))
return (cleaned:gsub("%-+", "-"))
end
return str

332
tests/crypto.lua Normal file
View File

@ -0,0 +1,332 @@
require("tests")
local crypto = require("crypto")
-- Test data
local test_data = "Hello, World!"
local test_key = "secret-key-123"
-- ======================================================================
-- ENCODING/DECODING TESTS
-- ======================================================================
test("Base64 Encoding/Decoding", function()
local encoded = crypto.base64_encode(test_data)
assert_equal(type(encoded), "string")
assert(#encoded > 0, "encoded string should not be empty")
local decoded = crypto.base64_decode(encoded)
assert_equal(decoded, test_data)
end)
test("Base64 URL Encoding/Decoding", function()
local encoded = crypto.base64_url_encode(test_data)
assert_equal(type(encoded), "string")
local decoded = crypto.base64_url_decode(encoded)
assert_equal(decoded, test_data)
end)
test("Hex Encoding/Decoding", function()
local encoded = crypto.hex_encode(test_data)
assert_equal(type(encoded), "string")
assert(encoded:match("^[0-9a-f]+$"), "hex should only contain hex characters")
local decoded = crypto.hex_decode(encoded)
assert_equal(decoded, test_data)
end)
test("Encoding Chain", function()
local chain_encoded = crypto.encode_chain(test_data, {"hex", "base64"})
local chain_decoded = crypto.decode_chain(chain_encoded, {"hex", "base64"})
assert_equal(chain_decoded, test_data)
end)
-- ======================================================================
-- HASHING TESTS
-- ======================================================================
test("MD5 Hash", function()
local hash = crypto.md5(test_data)
assert_equal(type(hash), "string")
assert_equal(#hash, 32) -- MD5 is 32 hex characters
assert(hash:match("^[0-9a-f]+$"), "hash should be hex")
-- Same input should produce same hash
assert_equal(crypto.md5(test_data), hash)
end)
test("SHA1 Hash", function()
local hash = crypto.sha1(test_data)
assert_equal(type(hash), "string")
assert_equal(#hash, 40) -- SHA1 is 40 hex characters
assert(hash:match("^[0-9a-f]+$"), "hash should be hex")
end)
test("SHA256 Hash", function()
local hash = crypto.sha256(test_data)
assert_equal(type(hash), "string")
assert_equal(#hash, 64) -- SHA256 is 64 hex characters
assert(hash:match("^[0-9a-f]+$"), "hash should be hex")
end)
test("SHA512 Hash", function()
local hash = crypto.sha512(test_data)
assert_equal(type(hash), "string")
assert_equal(#hash, 128) -- SHA512 is 128 hex characters
assert(hash:match("^[0-9a-f]+$"), "hash should be hex")
end)
test("Hash Multiple Inputs", function()
local hash1 = crypto.hash_multiple({"hello", "world"})
local hash2 = crypto.hash_multiple({"hello", "world"})
local hash3 = crypto.hash_multiple({"world", "hello"})
assert_equal(hash1, hash2)
assert(hash1 ~= hash3, "different order should produce different hash")
end)
-- ======================================================================
-- HMAC TESTS
-- ======================================================================
test("HMAC SHA1", function()
local hmac = crypto.hmac_sha1(test_data, test_key)
assert_equal(type(hmac), "string")
assert_equal(#hmac, 40) -- SHA1 HMAC is 40 hex characters
assert(hmac:match("^[0-9a-f]+$"), "hmac should be hex")
end)
test("HMAC SHA256", function()
local hmac = crypto.hmac_sha256(test_data, test_key)
assert_equal(type(hmac), "string")
assert_equal(#hmac, 64) -- SHA256 HMAC is 64 hex characters
assert(hmac:match("^[0-9a-f]+$"), "hmac should be hex")
end)
test("MAC Functions", function()
local mac = crypto.mac(test_data, test_key)
assert_equal(type(mac), "string")
assert(#mac > 0, "mac should not be empty")
local valid = crypto.verify_mac(test_data, test_key, mac)
assert_equal(valid, true)
local invalid = crypto.verify_mac("different data", test_key, mac)
assert_equal(invalid, false)
end)
-- ======================================================================
-- UUID TESTS
-- ======================================================================
test("UUID Generation", function()
local uuid1 = crypto.uuid()
local uuid2 = crypto.uuid_v4()
assert_equal(type(uuid1), "string")
assert_equal(type(uuid2), "string")
assert_equal(#uuid1, 36) -- Standard UUID length
assert_equal(#uuid2, 36)
assert(uuid1 ~= uuid2, "UUIDs should be unique")
assert_equal(crypto.is_uuid(uuid1), true)
assert_equal(crypto.is_uuid(uuid2), true)
assert_equal(crypto.is_uuid("not-a-uuid"), false)
end)
-- ======================================================================
-- RANDOM GENERATION TESTS
-- ======================================================================
test("Random Bytes", function()
local bytes1 = crypto.random_bytes(16)
local bytes2 = crypto.random_bytes(16)
assert_equal(type(bytes1), "string")
assert_equal(#bytes1, 16)
assert(bytes1 ~= bytes2, "random bytes should be different")
end)
test("Random Hex", function()
local hex1 = crypto.random_hex(8)
local hex2 = crypto.random_hex(8)
assert_equal(type(hex1), "string")
assert_equal(#hex1, 16) -- 8 bytes = 16 hex characters
assert(hex1:match("^[0-9a-f]+$"), "should be hex")
assert(hex1 ~= hex2, "random hex should be different")
end)
test("Random String", function()
local str1 = crypto.random_string(10)
local str2 = crypto.random_string(10)
assert_equal(type(str1), "string")
assert_equal(#str1, 10)
assert(str1 ~= str2, "random strings should be different")
local custom = crypto.random_string(5, "abc")
assert_equal(#custom, 5)
assert(custom:match("^[abc]+$"), "should only contain specified characters")
end)
test("Random Alphanumeric", function()
local str = crypto.random_alphanumeric(20)
assert_equal(#str, 20)
assert(str:match("^[a-zA-Z0-9]+$"), "should be alphanumeric")
end)
test("Random Password", function()
local pass1 = crypto.random_password(12)
local pass2 = crypto.random_password(12, true) -- with symbols
assert_equal(#pass1, 12)
assert_equal(#pass2, 12)
assert(pass1 ~= pass2, "passwords should be different")
end)
test("Token Generation", function()
local token1 = crypto.token(32)
local token2 = crypto.token(32)
assert_equal(#token1, 64) -- 32 bytes = 64 hex characters
assert(token1:match("^[0-9a-f]+$"), "token should be hex")
assert(token1 ~= token2, "tokens should be unique")
end)
test("Nonce Generation", function()
local nonce1 = crypto.nonce()
local nonce2 = crypto.nonce(32)
assert_equal(#nonce1, 32) -- default 16 bytes = 32 hex
assert_equal(#nonce2, 64) -- 32 bytes = 64 hex
assert(nonce1 ~= nonce2, "nonces should be unique")
end)
-- ======================================================================
-- UTILITY TESTS
-- ======================================================================
test("Secure Compare", function()
local str1 = "hello"
local str2 = "hello"
local str3 = "world"
assert_equal(crypto.secure_compare(str1, str2), true)
assert_equal(crypto.secure_compare(str1, str3), false)
end)
test("Checksum Functions", function()
local checksum = crypto.checksum(test_data)
assert_equal(type(checksum), "string")
local valid = crypto.verify_checksum(test_data, checksum)
assert_equal(valid, true)
local invalid = crypto.verify_checksum("different", checksum)
assert_equal(invalid, false)
end)
test("XOR Encryption", function()
local key = "mykey"
local encrypted = crypto.xor_encrypt(test_data, key)
local decrypted = crypto.xor_decrypt(encrypted, key)
assert_equal(decrypted, test_data)
assert(encrypted ~= test_data, "encrypted should be different")
end)
test("Hash Chain", function()
local chain1 = crypto.hash_chain(test_data, 100)
local chain2 = crypto.hash_chain(test_data, 100)
local chain3 = crypto.hash_chain(test_data, 101)
assert_equal(chain1, chain2)
assert(chain1 ~= chain3, "different iterations should produce different results")
end)
test("Key Derivation", function()
local derived1, salt1 = crypto.derive_key("password", "salt", 1000)
local derived2, salt2 = crypto.derive_key("password", salt1, 1000)
assert_equal(derived1, derived2)
assert_equal(salt1, salt2)
assert_equal(type(derived1), "string")
assert(#derived1 > 0, "derived key should not be empty")
end)
test("Fingerprint", function()
local data = {name = "test", value = 42}
local fp1 = crypto.fingerprint(data)
local fp2 = crypto.fingerprint(data)
assert_equal(fp1, fp2)
assert_equal(type(fp1), "string")
assert(#fp1 > 0, "fingerprint should not be empty")
end)
test("Integrity Check", function()
local check = crypto.integrity_check(test_data)
assert_equal(check.data, test_data)
assert_equal(type(check.hash), "string")
assert_equal(type(check.timestamp), "number")
assert_equal(type(check.uuid), "string")
local valid = crypto.verify_integrity(check)
assert_equal(valid, true)
-- Tamper with data
check.data = "tampered"
local invalid = crypto.verify_integrity(check)
assert_equal(invalid, false)
end)
-- ======================================================================
-- ERROR HANDLING TESTS
-- ======================================================================
test("Error Handling", function()
-- Invalid base64
local success, err = pcall(crypto.base64_decode, "invalid===base64")
assert_equal(success, false)
-- Invalid hex
local success2, err2 = pcall(crypto.hex_decode, "invalid_hex")
assert_equal(success2, false)
-- Invalid UUID validation (returns boolean, doesn't throw)
assert_equal(crypto.is_uuid("not-a-uuid"), false)
assert_equal(crypto.is_uuid(""), false)
assert_equal(crypto.is_uuid("12345"), false)
end)
-- ======================================================================
-- PERFORMANCE TESTS
-- ======================================================================
test("Performance Test", function()
local large_data = string.rep("test data for performance ", 1000)
local start = os.clock()
local hash = crypto.sha256(large_data)
local hash_time = os.clock() - start
start = os.clock()
local encoded = crypto.base64_encode(large_data)
local encode_time = os.clock() - start
start = os.clock()
local decoded = crypto.base64_decode(encoded)
local decode_time = os.clock() - start
print(string.format(" SHA256 of %d bytes: %.3fs", #large_data, hash_time))
print(string.format(" Base64 encode: %.3fs", encode_time))
print(string.format(" Base64 decode: %.3fs", decode_time))
assert_equal(decoded, large_data)
assert_equal(type(hash), "string")
end)
summary()
test_exit()

456
tests/fs.lua Normal file
View File

@ -0,0 +1,456 @@
require("tests")
local fs = require("fs")
-- Test data
local test_content = "Hello, filesystem!\nThis is a test file.\n"
local test_dir = "test_fs_dir"
local test_file = fs.join(test_dir, "test.txt")
-- Clean up function
local function cleanup()
if fs.exists(test_file) then fs.remove(test_file) end
if fs.exists(test_dir) then fs.rmdir(test_dir) end
end
-- ======================================================================
-- SETUP AND CLEANUP
-- ======================================================================
-- Clean up before tests
cleanup()
-- ======================================================================
-- BASIC FILE OPERATIONS
-- ======================================================================
test("File Write and Read", function()
fs.mkdir(test_dir)
fs.write(test_file, test_content)
assert_equal(fs.exists(test_file), true)
assert_equal(fs.is_file(test_file), true)
assert_equal(fs.is_dir(test_file), false)
local content = fs.read(test_file)
assert_equal(content, test_content)
end)
test("File Size", function()
local size = fs.size(test_file)
assert_equal(size, #test_content)
assert_equal(fs.size("nonexistent.txt"), nil)
end)
test("File Append", function()
local additional = "Appended content.\n"
fs.append(test_file, additional)
local content = fs.read(test_file)
assert_equal(content, test_content .. additional)
end)
test("File Copy", function()
local copy_file = fs.join(test_dir, "copy.txt")
fs.copy(test_file, copy_file)
assert_equal(fs.exists(copy_file), true)
assert_equal(fs.read(copy_file), fs.read(test_file))
fs.remove(copy_file)
end)
test("File Move", function()
local move_file = fs.join(test_dir, "moved.txt")
local original_content = fs.read(test_file)
fs.move(test_file, move_file)
assert_equal(fs.exists(test_file), false)
assert_equal(fs.exists(move_file), true)
assert_equal(fs.read(move_file), original_content)
-- Move back for other tests
fs.move(move_file, test_file)
end)
test("File Lines", function()
local lines = fs.lines(test_file)
assert_equal(type(lines), "table")
assert(#lines >= 2, "should have multiple lines")
assert(string.find(lines[1], "Hello"), "first line should contain 'Hello'")
end)
test("File Touch", function()
local touch_file = fs.join(test_dir, "touched.txt")
fs.touch(touch_file)
assert_equal(fs.exists(touch_file), true)
assert_equal(fs.size(touch_file), 0)
fs.remove(touch_file)
end)
test("File Modification Time", function()
local mtime = fs.mtime(test_file)
assert_equal(type(mtime), "number")
assert(mtime > 0, "mtime should be positive")
-- Touch should update mtime
local old_mtime = mtime
fs.touch(test_file)
local new_mtime = fs.mtime(test_file)
assert(new_mtime >= old_mtime, "mtime should be updated")
end)
-- ======================================================================
-- DIRECTORY OPERATIONS
-- ======================================================================
test("Directory Creation and Removal", function()
local nested_dir = fs.join(test_dir, "nested", "deep")
fs.mkdir(nested_dir)
assert_equal(fs.exists(nested_dir), true)
assert_equal(fs.is_dir(nested_dir), true)
-- Clean up nested directories
fs.rmdir(fs.join(test_dir, "nested"))
end)
test("Directory Listing", function()
-- Create some test files
fs.write(fs.join(test_dir, "file1.txt"), "content1")
fs.write(fs.join(test_dir, "file2.log"), "content2")
fs.mkdir(fs.join(test_dir, "subdir"))
local entries = fs.list(test_dir)
assert_equal(type(entries), "table")
assert(#entries >= 3, "should have at least 3 entries")
-- Check entry structure
local found_file = false
for _, entry in ipairs(entries) do
assert_equal(type(entry.name), "string")
assert_equal(type(entry.is_dir), "boolean")
if entry.name == "file1.txt" then
found_file = true
assert_equal(entry.is_dir, false)
assert_equal(type(entry.size), "number")
end
end
assert_equal(found_file, true)
-- Test filtered listings
local files = fs.list_files(test_dir)
local dirs = fs.list_dirs(test_dir)
local names = fs.list_names(test_dir)
assert_equal(type(files), "table")
assert_equal(type(dirs), "table")
assert_equal(type(names), "table")
assert(#files > 0, "should have files")
assert(#dirs > 0, "should have directories")
-- Clean up
fs.remove(fs.join(test_dir, "file1.txt"))
fs.remove(fs.join(test_dir, "file2.log"))
fs.rmdir(fs.join(test_dir, "subdir"))
end)
-- ======================================================================
-- PATH OPERATIONS
-- ======================================================================
test("Path Join", function()
local path = fs.join("a", "b", "c", "file.txt")
assert(string.find(path, "file.txt"), "should contain filename")
local empty_path = fs.join()
assert_equal(type(empty_path), "string")
end)
test("Path Components", function()
local test_path = fs.join("home", "user", "documents", "file.txt")
local dir = fs.dirname(test_path)
local base = fs.basename(test_path)
local ext = fs.ext(test_path)
assert_equal(base, "file.txt")
assert_equal(ext, ".txt")
assert(string.find(dir, "documents"), "dirname should contain 'documents'")
end)
test("Path Split Extension", function()
local test_path = fs.join("home", "user", "file.tar.gz")
local dir, name, ext = fs.splitext(test_path)
assert_equal(name, "file.tar")
assert_equal(ext, ".gz")
assert(string.find(dir, "user"), "dir should contain 'user'")
end)
test("Path Absolute", function()
local abs_path = fs.abs(".")
assert_equal(type(abs_path), "string")
assert(#abs_path > 1, "absolute path should not be empty")
end)
test("Path Clean", function()
local messy_path = "./test/../test/./file.txt"
local clean_path = fs.clean(messy_path)
assert_equal(type(clean_path), "string")
assert(not string.find(clean_path, "%.%."), "should not contain '..'")
end)
test("Path Split", function()
local test_path = fs.join("home", "user", "file.txt")
local dir, file = fs.split(test_path)
assert_equal(file, "file.txt")
assert(string.find(dir, "user"), "dir should contain 'user'")
end)
-- ======================================================================
-- WORKING DIRECTORY
-- ======================================================================
test("Working Directory", function()
local original_cwd = fs.getcwd()
assert_equal(type(original_cwd), "string")
assert(#original_cwd > 0, "cwd should not be empty")
-- Test directory change
fs.chdir(test_dir)
local new_cwd = fs.getcwd()
assert(string.find(new_cwd, test_dir), "cwd should contain test_dir")
-- Change back
fs.chdir(original_cwd)
assert_equal(fs.getcwd(), original_cwd)
end)
-- ======================================================================
-- TEMPORARY FILES
-- ======================================================================
test("Temporary Files", function()
local temp_file = fs.tempfile("test_")
local temp_dir = fs.tempdir("test_")
assert_equal(type(temp_file), "string")
assert_equal(type(temp_dir), "string")
assert_equal(fs.exists(temp_file), true)
assert_equal(fs.exists(temp_dir), true)
assert_equal(fs.is_dir(temp_dir), true)
-- Clean up
fs.remove(temp_file)
fs.rmdir(temp_dir)
end)
-- ======================================================================
-- PATTERN MATCHING
-- ======================================================================
test("Glob Patterns", function()
-- Create test files for globbing
fs.write(fs.join(test_dir, "test1.txt"), "content")
fs.write(fs.join(test_dir, "test2.txt"), "content")
fs.write(fs.join(test_dir, "other.log"), "content")
local pattern = fs.join(test_dir, "*.txt")
local matches = fs.glob(pattern)
assert_equal(type(matches), "table")
assert(#matches >= 2, "should match txt files")
-- Clean up
fs.remove(fs.join(test_dir, "test1.txt"))
fs.remove(fs.join(test_dir, "test2.txt"))
fs.remove(fs.join(test_dir, "other.log"))
end)
test("Walk Directory", function()
-- Create nested structure
fs.mkdir(fs.join(test_dir, "sub1"))
fs.mkdir(fs.join(test_dir, "sub2"))
fs.write(fs.join(test_dir, "root.txt"), "content")
fs.write(fs.join(test_dir, "sub1", "nested.txt"), "content")
local files = fs.walk(test_dir)
assert_equal(type(files), "table")
assert(#files > 3, "should find multiple files and directories")
-- Clean up
fs.remove(fs.join(test_dir, "root.txt"))
fs.remove(fs.join(test_dir, "sub1", "nested.txt"))
fs.rmdir(fs.join(test_dir, "sub1"))
fs.rmdir(fs.join(test_dir, "sub2"))
end)
-- ======================================================================
-- UTILITY FUNCTIONS
-- ======================================================================
test("File Extension Functions", function()
local path = "document.pdf"
assert_equal(fs.extension(path), "pdf")
local new_path = fs.change_ext(path, "txt")
assert_equal(new_path, "document.txt")
local new_path2 = fs.change_ext(path, ".docx")
assert_equal(new_path2, "document.docx")
end)
test("Ensure Directory", function()
local ensure_dir = fs.join(test_dir, "ensure_test")
fs.ensure_dir(ensure_dir)
assert_equal(fs.exists(ensure_dir), true)
assert_equal(fs.is_dir(ensure_dir), true)
-- Should not error if already exists
fs.ensure_dir(ensure_dir)
fs.rmdir(ensure_dir)
end)
test("Human Readable Size", function()
local small_file = fs.join(test_dir, "small.txt")
fs.write(small_file, "test")
local size_str = fs.size_human(small_file)
assert_equal(type(size_str), "string")
assert(string.find(size_str, "B"), "should contain byte unit")
fs.remove(small_file)
end)
test("Safe Path Check", function()
assert_equal(fs.is_safe_path("safe/path.txt"), true)
assert_equal(fs.is_safe_path("../dangerous"), false)
assert_equal(fs.is_safe_path("/absolute/path"), false)
assert_equal(fs.is_safe_path("~/home/path"), false)
end)
test("Copy Tree", function()
-- Create source structure
local src_dir = fs.join(test_dir, "src")
local dst_dir = fs.join(test_dir, "dst")
fs.mkdir(src_dir)
fs.mkdir(fs.join(src_dir, "subdir"))
fs.write(fs.join(src_dir, "file1.txt"), "content1")
fs.write(fs.join(src_dir, "subdir", "file2.txt"), "content2")
fs.copytree(src_dir, dst_dir)
assert_equal(fs.exists(dst_dir), true)
assert_equal(fs.exists(fs.join(dst_dir, "file1.txt")), true)
assert_equal(fs.exists(fs.join(dst_dir, "subdir", "file2.txt")), true)
assert_equal(fs.read(fs.join(dst_dir, "file1.txt")), "content1")
-- Clean up
fs.rmdir(src_dir)
fs.rmdir(dst_dir)
end)
test("Find Files", function()
-- Create test files
fs.write(fs.join(test_dir, "find1.txt"), "content")
fs.write(fs.join(test_dir, "find2.txt"), "content")
fs.write(fs.join(test_dir, "other.log"), "content")
fs.mkdir(fs.join(test_dir, "subdir"))
fs.write(fs.join(test_dir, "subdir", "find3.txt"), "content")
local txt_files = fs.find(test_dir, "%.txt$", true)
assert_equal(type(txt_files), "table")
assert(#txt_files >= 3, "should find txt files recursively")
local txt_files_flat = fs.find(test_dir, "%.txt$", false)
assert(#txt_files_flat < #txt_files, "non-recursive should find fewer files")
-- Clean up
fs.remove(fs.join(test_dir, "find1.txt"))
fs.remove(fs.join(test_dir, "find2.txt"))
fs.remove(fs.join(test_dir, "other.log"))
fs.remove(fs.join(test_dir, "subdir", "find3.txt"))
fs.rmdir(fs.join(test_dir, "subdir"))
end)
test("Directory Tree", function()
-- Create test structure
fs.mkdir(fs.join(test_dir, "tree_test"))
fs.write(fs.join(test_dir, "tree_test", "file.txt"), "content")
fs.mkdir(fs.join(test_dir, "tree_test", "subdir"))
local tree = fs.tree(test_dir)
assert_equal(type(tree), "table")
assert_equal(tree.is_dir, true)
assert_equal(type(tree.children), "table")
assert(#tree.children > 0, "should have children")
-- Clean up
fs.remove(fs.join(test_dir, "tree_test", "file.txt"))
fs.rmdir(fs.join(test_dir, "tree_test", "subdir"))
fs.rmdir(fs.join(test_dir, "tree_test"))
end)
-- ======================================================================
-- ERROR HANDLING
-- ======================================================================
test("Error Handling", function()
-- Reading non-existent file
local success, err = pcall(fs.read, "nonexistent.txt")
assert_equal(success, false)
-- Writing to invalid path
local success2, err2 = pcall(fs.write, "/invalid/path/file.txt", "content")
assert_equal(success2, false)
-- Listing non-existent directory
local success3, err3 = pcall(fs.list, "nonexistent_dir")
assert_equal(success3, false)
end)
-- ======================================================================
-- PERFORMANCE TESTS
-- ======================================================================
test("Performance Test", function()
local large_content = string.rep("performance test data\n", 1000)
local perf_file = fs.join(test_dir, "performance.txt")
local start = os.clock()
fs.write(perf_file, large_content)
local write_time = os.clock() - start
start = os.clock()
local read_content = fs.read(perf_file)
local read_time = os.clock() - start
start = os.clock()
local lines = fs.lines(perf_file)
local lines_time = os.clock() - start
print(string.format(" Write %d bytes: %.3fs", #large_content, write_time))
print(string.format(" Read %d bytes: %.3fs", #read_content, read_time))
print(string.format(" Parse %d lines: %.3fs", #lines, lines_time))
assert_equal(read_content, large_content)
assert_equal(#lines, 1000)
fs.remove(perf_file)
end)
-- ======================================================================
-- CLEANUP
-- ======================================================================
cleanup()
summary()
test_exit()

486
tests/string.lua Normal file
View File

@ -0,0 +1,486 @@
require("tests")
local str = require("string")
-- Test data
local test_string = "Hello, World!"
local multi_line = "Line 1\nLine 2\nLine 3"
local padded_string = " Hello World "
-- ======================================================================
-- BASIC STRING OPERATIONS
-- ======================================================================
test("String Split and Join", function()
local parts = str.split("a,b,c,d", ",")
assert_equal("table", type(parts))
assert_equal(4, #parts)
assert_equal("a", parts[1])
assert_equal("d", parts[4])
local joined = str.join(parts, "-")
assert_equal("a-b-c-d", joined)
-- Test empty split
local empty_parts = str.split("", ",")
assert_equal(1, #empty_parts)
assert_equal("", empty_parts[1])
end)
test("String Trim Operations", function()
assert_equal("Hello World", str.trim(padded_string))
assert_equal("Hello World ", str.trim_left(padded_string))
assert_equal(" Hello World", str.trim_right(padded_string))
-- Custom cutset
assert_equal("Helloxxx", str.trim_left("xxxHelloxxx", "x"))
assert_equal("xxxHello", str.trim_right("xxxHelloxxx", "x"))
end)
test("Case Operations", function()
assert_equal("HELLO", str.upper("hello"))
assert_equal("hello", str.lower("HELLO"))
assert_equal("Hello World", str.title("hello world"))
-- Test with mixed content
assert_equal("HELLO123!", str.upper("Hello123!"))
assert_equal("hello123!", str.lower("HELLO123!"))
end)
test("String Contains and Position", function()
assert_equal(true, str.contains(test_string, "World"))
assert_equal(false, str.contains(test_string, "world"))
assert_equal(true, str.starts_with(test_string, "Hello"))
assert_equal(false, str.starts_with(test_string, "hello"))
assert_equal(true, str.ends_with(test_string, "!"))
assert_equal(false, str.ends_with(test_string, "?"))
end)
test("String Replace", function()
assert_equal("hi world hi", str.replace("hello world hello", "hello", "hi"))
assert_equal("hi world hello", str.replace_n("hello world hello", "hello", "hi", 1))
-- Test with no matches
assert_equal("hello", str.replace("hello", "xyz", "abc"))
end)
test("String Index Operations", function()
assert_equal(7, str.index("hello world", "world"))
assert_equal(nil, str.index("hello world", "xyz"))
assert_equal(7, str.last_index("hello hello", "hello"))
assert_equal(3, str.count("hello hello hello", "hello"))
end)
test("String Repeat and Reverse", function()
assert_equal("abcabcabc", str.repeat_("abc", 3))
assert_equal("", str.repeat_("x", 0))
assert_equal("olleh", str.reverse("hello"))
assert_equal("", str.reverse(""))
end)
test("String Length Operations", function()
assert_equal(5, str.length("hello"))
assert_equal(5, str.byte_length("hello"))
assert_equal(0, str.length(""))
-- Test Unicode
local unicode_str = "héllo"
assert_equal(5, str.length(unicode_str))
assert_equal(6, str.byte_length(unicode_str)) -- é takes 2 bytes in UTF-8
end)
test("String Lines and Words", function()
local lines = str.lines(multi_line)
assert_equal(3, #lines)
assert_equal("Line 1", lines[1])
assert_equal("Line 3", lines[3])
local words = str.words("Hello world test")
assert_equal(3, #words)
assert_equal("Hello", words[1])
assert_equal("test", words[3])
-- Test with extra whitespace
local words2 = str.words(" Hello world ")
assert_equal(2, #words2)
end)
test("String Padding", function()
assert_equal(" hi", str.pad_left("hi", 5))
assert_equal("hi ", str.pad_right("hi", 5))
assert_equal("000hi", str.pad_left("hi", 5, "0"))
assert_equal("hi***", str.pad_right("hi", 5, "*"))
-- Test when string is already long enough
assert_equal("hello", str.pad_left("hello", 3))
end)
test("String Slice", function()
assert_equal("ell", str.slice("hello", 2, 4))
assert_equal("ello", str.slice("hello", 2))
assert_equal("", str.slice("hello", 10))
assert_equal("h", str.slice("hello", 1, 1))
end)
-- ======================================================================
-- REGULAR EXPRESSIONS
-- ======================================================================
test("Regex Match", function()
assert_equal(true, str.match("\\d+", "hello123"))
assert_equal(false, str.match("\\d+", "hello"))
assert_equal(true, str.match("^[a-z]+$", "hello"))
assert_equal(false, str.match("^[a-z]+$", "Hello"))
end)
test("Regex Find", function()
assert_equal("123", str.find("\\d+", "hello123world"))
assert_equal(nil, str.find("\\d+", "hello"))
local matches = str.find_all("\\d+", "123 and 456 and 789")
assert_equal(3, #matches)
assert_equal("123", matches[1])
assert_equal("789", matches[3])
end)
test("Regex Replace", function()
assert_equal("helloXXXworldXXX", str.gsub("\\d+", "hello123world456", "XXX"))
assert_equal("hello world", str.gsub("\\s+", "hello world", " "))
end)
-- ======================================================================
-- TYPE CONVERSION & VALIDATION
-- ======================================================================
test("String to Number", function()
assert_equal(123, str.to_number("123"))
assert_equal(123.45, str.to_number("123.45"))
assert_equal(-42, str.to_number("-42"))
assert_equal(nil, str.to_number("not_a_number"))
end)
test("String Validation", function()
assert_equal(true, str.is_numeric("123"))
assert_equal(true, str.is_numeric("123.45"))
assert_equal(false, str.is_numeric("abc"))
assert_equal(true, str.is_alpha("hello"))
assert_equal(false, str.is_alpha("hello123"))
assert_equal(false, str.is_alpha(""))
assert_equal(true, str.is_alphanumeric("hello123"))
assert_equal(false, str.is_alphanumeric("hello!"))
assert_equal(false, str.is_alphanumeric(""))
assert_equal(true, str.is_empty(""))
assert_equal(true, str.is_empty(nil))
assert_equal(false, str.is_empty("hello"))
assert_equal(true, str.is_blank(""))
assert_equal(true, str.is_blank(" "))
assert_equal(false, str.is_blank("hello"))
end)
-- ======================================================================
-- ADVANCED STRING OPERATIONS
-- ======================================================================
test("Case Conversion Functions", function()
assert_equal("Hello World", str.capitalize("hello world"))
assert_equal("helloWorld", str.camel_case("hello world"))
assert_equal("HelloWorld", str.pascal_case("hello world"))
assert_equal("hello_world", str.snake_case("Hello World"))
assert_equal("hello-world", str.kebab_case("Hello World"))
assert_equal("HELLO_WORLD", str.screaming_snake_case("hello world"))
end)
test("String Center and Truncate", function()
assert_equal(" hi ", str.center("hi", 6))
assert_equal("**hi***", str.center("hi", 7, "*"))
assert_equal("hello", str.center("hello", 3)) -- Already longer
assert_equal("hello...", str.truncate("hello world", 8))
assert_equal("hello>>", str.truncate("hello world", 8, ">>"))
assert_equal("hi", str.truncate("hi", 10)) -- Shorter than limit
end)
test("String Wrap", function()
local wrapped = str.wrap("The quick brown fox jumps over the lazy dog", 10)
assert_equal("table", type(wrapped))
assert(#wrapped > 1, "should wrap into multiple lines")
-- Each line should be within limit
for _, line in ipairs(wrapped) do
assert(str.length(line) <= 10, "line should be within width limit")
end
end)
test("String Dedent", function()
local indented = " line1\n line2\n line3"
local dedented = str.dedent(indented)
local lines = str.lines(dedented)
assert_equal("line1", lines[1])
assert_equal("line2", lines[2])
assert_equal("line3", lines[3])
end)
test("Escape and Quote Functions", function()
assert_equal("hello\\.world", str.escape_regex("hello.world"))
assert_equal("a\\+b\\*c\\?", str.escape_regex("a+b*c?"))
assert_equal("'hello world'", str.shell_quote("hello world"))
assert_equal("'it'\"'\"'s great'", str.shell_quote("it's great"))
end)
test("URL Encoding", function()
assert_equal("hello%20world", str.url_encode("hello world"))
assert_equal("caf%C3%A9", str.url_encode("café"))
local encoded = str.url_encode("hello world")
assert_equal("hello world", str.url_decode(encoded))
assert_equal("hello world", str.url_decode("hello+world"))
end)
-- ======================================================================
-- STRING COMPARISON
-- ======================================================================
test("String Comparison", function()
assert_equal(true, str.iequals("Hello", "HELLO"))
assert_equal(false, str.iequals("Hello", "world"))
-- Test distance and similarity
assert_equal(3, str.distance("kitten", "sitting"))
assert_equal(0, str.distance("hello", "hello"))
local similarity = str.similarity("hello", "hallo")
assert(similarity > 0.5 and similarity < 1, "should be partial similarity")
assert_equal(1, str.similarity("hello", "hello"))
end)
-- ======================================================================
-- TEMPLATE FUNCTIONS
-- ======================================================================
test("Template Functions", function()
local simple_template = "Hello ${name}, you are ${age} years old"
local vars = {name = "John", age = 25}
assert_equal("Hello John, you are 25 years old", str.template(simple_template, vars))
-- Test with missing variables
local incomplete = str.template("Hello ${name} and ${unknown}", {name = "John"})
assert_equal("Hello John and ", incomplete)
-- Advanced template
local context = {
user = {name = "Jane", role = "admin"},
count = 5
}
local advanced = str.template_advanced("User ${user.name} (${user.role}) has ${count} items", context)
assert_equal("User Jane (admin) has 5 items", advanced)
end)
-- ======================================================================
-- UTILITY FUNCTIONS
-- ======================================================================
test("Whitespace Functions", function()
assert_equal(true, str.is_whitespace(" "))
assert_equal(true, str.is_whitespace(""))
assert_equal(false, str.is_whitespace("hello"))
assert_equal("hello", str.strip_whitespace("h e l l o"))
assert_equal("hello world test", str.normalize_whitespace("hello world test"))
end)
test("Number Extraction", function()
local numbers = str.extract_numbers("The price is $123.45 and tax is 8.5%")
assert_equal(2, #numbers)
assert_close(123.45, numbers[1])
assert_close(8.5, numbers[2])
local negative_nums = str.extract_numbers("Temperature: -15.5 degrees")
assert_equal(1, #negative_nums)
assert_close(-15.5, negative_nums[1])
end)
test("Accent Removal", function()
assert_equal("cafe", str.remove_accents("café"))
assert_equal("resume", str.remove_accents("résumé"))
assert_equal("naive", str.remove_accents("naïve"))
assert_equal("hello", str.remove_accents("hello"))
end)
test("Random String Generation", function()
local random1 = str.random(10)
local random2 = str.random(10)
assert_equal(10, str.length(random1))
assert_equal(10, str.length(random2))
assert(random1 ~= random2, "random strings should be different")
-- Custom charset
local custom = str.random(5, "abc")
assert_equal(5, str.length(custom))
assert(str.match("^[abc]+$", custom), "should only contain specified characters")
end)
test("UTF-8 Validation", function()
assert_equal(true, str.is_utf8("hello"))
assert_equal(true, str.is_utf8("café"))
assert_equal(true, str.is_utf8(""))
-- Note: This test depends on the actual UTF-8 validation implementation
-- Some invalid UTF-8 sequences might still pass depending on the system
end)
test("Slug Generation", function()
assert_equal("hello-world", str.slug("Hello World"))
assert_equal("cafe-restaurant", str.slug("Café & Restaurant"))
assert_equal("specialcharacters", str.slug("Special!@#$%Characters"))
end)
-- ======================================================================
-- EDGE CASES AND ERROR HANDLING
-- ======================================================================
test("Empty String Handling", function()
assert_table_equal({""}, str.split("", ","))
assert_equal("", str.join({}, ","))
assert_equal("", str.trim(""))
assert_equal("", str.reverse(""))
assert_equal("", str.repeat_("", 5))
assert_table_equal({""}, str.lines(""))
assert_table_equal({}, str.words(""))
end)
test("Large String Handling", function()
local large_string = string.rep("test ", 1000)
assert_equal(5000, str.length(large_string))
assert_equal(1000, str.count(large_string, "test"))
local words = str.words(large_string)
assert_equal(1000, #words)
local trimmed = str.trim(large_string)
assert_equal(true, str.ends_with(trimmed, "test"))
end)
test("Unicode Handling", function()
local unicode_string = "Hello 🌍 World 🚀"
-- Basic operations should work with Unicode
assert_equal(true, str.contains(unicode_string, "🌍"))
assert_equal(str.upper(unicode_string), str.upper(unicode_string)) -- Should not crash
local parts = str.split(unicode_string, " ")
assert_equal(4, #parts)
assert_equal("🌍", parts[2])
end)
test("Regex Error Handling", function()
-- Invalid regex pattern - check if it actually fails
local success, result = pcall(str.match, "\\", "test")
if success then
-- If it doesn't fail, just verify it works with valid patterns
assert_equal(true, str.match("test", "test"))
else
assert_equal(false, success)
end
local success2, result2 = pcall(str.find, "\\", "test")
if success2 then
-- If it doesn't fail, just verify it works with valid patterns
assert(str.find("test", "test") ~= nil)
else
assert_equal(false, success2)
end
end)
-- ======================================================================
-- PERFORMANCE TESTS
-- ======================================================================
test("Performance Test", function()
local large_text = string.rep("The quick brown fox jumps over the lazy dog. ", 1000)
local start = os.clock()
local words = str.words(large_text)
local words_time = os.clock() - start
start = os.clock()
local lines = str.lines(large_text)
local lines_time = os.clock() - start
start = os.clock()
local replaced = str.replace(large_text, "fox", "cat")
local replace_time = os.clock() - start
start = os.clock()
local parts = str.split(large_text, " ")
local split_time = os.clock() - start
print(string.format(" Extract %d words: %.3fs", #words, words_time))
print(string.format(" Extract %d lines: %.3fs", #lines, lines_time))
print(string.format(" Replace in %d chars: %.3fs", str.length(large_text), replace_time))
print(string.format(" Split into %d parts: %.3fs", #parts, split_time))
assert(#words > 8000, "should extract many words")
assert(str.contains(replaced, "cat"), "replacement should work")
end)
-- ======================================================================
-- INTEGRATION TESTS
-- ======================================================================
test("String Processing Pipeline", function()
local messy_input = " HELLO, world! How ARE you? "
-- Clean and normalize
local cleaned = str.normalize_whitespace(str.trim(messy_input))
local lowered = str.lower(cleaned)
local words = str.words(lowered)
local filtered = {}
for _, word in ipairs(words) do
-- Remove punctuation from word before checking length
local clean_word = str.gsub("[[:punct:]]", word, "")
if str.length(clean_word) > 2 then
table.insert(filtered, clean_word)
end
end
local result = str.join(filtered, "-")
assert_equal("hello-world-how-are-you", result)
end)
test("Text Analysis", function()
local text = "The quick brown fox jumps over the lazy dog. The dog was sleeping."
local word_count = #str.words(text)
local sentence_count = str.count(text, ".")
local the_count = str.count(str.lower(text), "the")
assert_equal(13, word_count)
assert_equal(2, sentence_count)
assert_equal(3, the_count)
-- Extract all words starting with vowels
local words = str.words(str.lower(text))
local vowel_words = {}
for _, word in ipairs(words) do
local clean_word = str.replace(word, "%p", "") -- Remove punctuation
if str.match("^[aeiou]", clean_word) then
table.insert(vowel_words, clean_word)
end
end
assert(#vowel_words >= 1, "should find words starting with vowels")
end)
summary()
test_exit()

View File

@ -23,57 +23,57 @@ function assert(condition, message, level)
end end
-- Assert with tolerance for floating point comparisons -- Assert with tolerance for floating point comparisons
function assert_close(a, b, tolerance, message) function assert_close(expected, actual, tolerance, message)
tolerance = tolerance or 1e-10 tolerance = tolerance or 1e-10
local diff = math.abs(a - b) local diff = math.abs(expected - actual)
if diff <= tolerance then if diff <= tolerance then
return true return true
end end
local msg = message or string.format("Expected %g, got %g (diff: %g, tolerance: %g)", a, b, diff, tolerance) local msg = message or string.format("Expected %g, got %g (diff: %g, tolerance: %g)", expected, actual, diff, tolerance)
assert(false, msg, 3) assert(false, msg, 3)
end end
-- Assert equality with better error messages -- Assert equality with better error messages
function assert_equal(a, b, message) function assert_equal(expected, actual, message)
if a == b then if expected == actual then
return true return true
end end
local msg = message or string.format("Expected %s, got %s", tostring(a), tostring(b)) local msg = message or string.format("Expected %s, got %s", tostring(expected), tostring(actual))
assert(false, msg, 3) assert(false, msg, 3)
end end
-- Assert table equality (deep comparison) -- Assert table equality (deep comparison)
function assert_table_equal(a, b, message, path) function assert_table_equal(expected, actual, message, path)
path = path or "root" path = path or "root"
if type(a) ~= type(b) then if type(expected) ~= type(actual) then
local msg = message or string.format("Type mismatch at %s: expected %s, got %s", path, type(a), type(b)) local msg = message or string.format("Type mismatch at %s: expected %s, got %s", path, type(expected), type(actual))
assert(false, msg, 3) assert(false, msg, 3)
end end
if type(a) ~= "table" then if type(expected) ~= "table" then
if a ~= b then if expected ~= actual then
local msg = message or string.format("Value mismatch at %s: expected %s, got %s", path, tostring(a), tostring(b)) local msg = message or string.format("Value mismatch at %s: expected %s, got %s", path, tostring(expected), tostring(actual))
assert(false, msg, 3) assert(false, msg, 3)
end end
return true return true
end end
-- Check all keys in a exist in b with same values -- Check all keys in a exist in b with same values
for k, v in pairs(a) do for k, v in pairs(expected) do
local new_path = path .. "." .. tostring(k) local new_path = path .. "." .. tostring(k)
if b[k] == nil then if actual[k] == nil then
local msg = message or string.format("Missing key at %s", new_path) local msg = message or string.format("Missing key at %s", new_path)
assert(false, msg, 3) assert(false, msg, 3)
end end
assert_table_equal(v, b[k], message, new_path) assert_table_equal(v, actual[k], message, new_path)
end end
-- Check all keys in b exist in a -- Check all keys in b exist in a
for k, v in pairs(b) do for k, v in pairs(actual) do
if a[k] == nil then if expected[k] == nil then
local new_path = path .. "." .. tostring(k) local new_path = path .. "." .. tostring(k)
local msg = message or string.format("Extra key at %s", new_path) local msg = message or string.format("Extra key at %s", new_path)
assert(false, msg, 3) assert(false, msg, 3)