417 lines
12 KiB
Go
417 lines
12 KiB
Go
package processor
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
logger "git.site.quack-lab.dev/dave/cylogger"
|
|
lua "github.com/yuin/gopher-lua"
|
|
)
|
|
|
|
// Maybe we make this an interface again for the shits and giggles
|
|
// We will see, it could easily be...
|
|
|
|
func NewLuaState() (*lua.LState, error) {
|
|
L := lua.NewState()
|
|
// defer L.Close()
|
|
|
|
// Load math library
|
|
logger.Debug("Loading Lua math library")
|
|
L.Push(L.GetGlobal("require"))
|
|
L.Push(lua.LString("math"))
|
|
if err := L.PCall(1, 1, nil); err != nil {
|
|
logger.Error("Failed to load Lua math library: %v", err)
|
|
return nil, fmt.Errorf("error loading Lua math library: %v", err)
|
|
}
|
|
|
|
// Initialize helper functions
|
|
logger.Debug("Initializing Lua helper functions")
|
|
if err := InitLuaHelpers(L); err != nil {
|
|
logger.Error("Failed to initialize Lua helper functions: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
return L, nil
|
|
}
|
|
|
|
// func Process(filename string, pattern string, luaExpr string) (int, int, error) {
|
|
// logger.Debug("Processing file %q with pattern %q", filename, pattern)
|
|
//
|
|
// // Read file content
|
|
// cwd, err := os.Getwd()
|
|
// if err != nil {
|
|
// logger.Error("Failed to get current working directory: %v", err)
|
|
// return 0, 0, fmt.Errorf("error getting current working directory: %v", err)
|
|
// }
|
|
//
|
|
// fullPath := filepath.Join(cwd, filename)
|
|
// logger.Trace("Reading file from: %s", fullPath)
|
|
//
|
|
// stat, err := os.Stat(fullPath)
|
|
// if err != nil {
|
|
// logger.Error("Failed to stat file %s: %v", fullPath, err)
|
|
// return 0, 0, fmt.Errorf("error getting file info: %v", err)
|
|
// }
|
|
// logger.Debug("File size: %d bytes, modified: %s", stat.Size(), stat.ModTime().Format(time.RFC3339))
|
|
//
|
|
// content, err := os.ReadFile(fullPath)
|
|
// if err != nil {
|
|
// logger.Error("Failed to read file %s: %v", fullPath, err)
|
|
// return 0, 0, fmt.Errorf("error reading file: %v", err)
|
|
// }
|
|
//
|
|
// fileContent := string(content)
|
|
// logger.Trace("File read successfully: %d bytes, hash: %x", len(content), md5sum(content))
|
|
//
|
|
// // Detect and log file type
|
|
// fileType := detectFileType(filename, fileContent)
|
|
// if fileType != "" {
|
|
// logger.Debug("Detected file type: %s", fileType)
|
|
// }
|
|
//
|
|
// // Process the content
|
|
// logger.Debug("Starting content processing")
|
|
// modifiedContent, modCount, matchCount, err := ProcessContent(fileContent, pattern, luaExpr)
|
|
// if err != nil {
|
|
// logger.Error("Processing error: %v", err)
|
|
// return 0, 0, err
|
|
// }
|
|
//
|
|
// logger.Debug("Processing results: %d matches, %d modifications", matchCount, modCount)
|
|
//
|
|
// // If we made modifications, save the file
|
|
// if modCount > 0 {
|
|
// // Calculate changes summary
|
|
// changePercent := float64(len(modifiedContent)) / float64(len(fileContent)) * 100
|
|
// logger.Info("File size change: %d → %d bytes (%.1f%%)",
|
|
// len(fileContent), len(modifiedContent), changePercent)
|
|
//
|
|
// logger.Debug("Writing modified content to %s", fullPath)
|
|
// err = os.WriteFile(fullPath, []byte(modifiedContent), 0644)
|
|
// if err != nil {
|
|
// logger.Error("Failed to write to file %s: %v", fullPath, err)
|
|
// return 0, 0, fmt.Errorf("error writing file: %v", err)
|
|
// }
|
|
// logger.Debug("File written successfully, new hash: %x", md5sum([]byte(modifiedContent)))
|
|
// } else if matchCount > 0 {
|
|
// logger.Debug("No content modifications needed for %d matches", matchCount)
|
|
// } else {
|
|
// logger.Debug("No matches found in file")
|
|
// }
|
|
//
|
|
// return modCount, matchCount, nil
|
|
// }
|
|
|
|
// FromLua converts a Lua table to a struct or map recursively
|
|
func FromLua(L *lua.LState, luaValue lua.LValue) (interface{}, error) {
|
|
switch v := luaValue.(type) {
|
|
// Well shit...
|
|
// Tables in lua are both maps and arrays
|
|
// As arrays they are ordered and as maps, obviously, not
|
|
// So when we parse them to a go map we fuck up the order for arrays
|
|
// We have to find a better way....
|
|
case *lua.LTable:
|
|
isArray, err := IsLuaTableArray(L, v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if isArray {
|
|
result := make([]interface{}, 0)
|
|
v.ForEach(func(key lua.LValue, value lua.LValue) {
|
|
converted, _ := FromLua(L, value)
|
|
result = append(result, converted)
|
|
})
|
|
return result, nil
|
|
} else {
|
|
result := make(map[string]interface{})
|
|
v.ForEach(func(key lua.LValue, value lua.LValue) {
|
|
converted, _ := FromLua(L, value)
|
|
result[key.String()] = converted
|
|
})
|
|
return result, nil
|
|
}
|
|
case lua.LString:
|
|
return string(v), nil
|
|
case lua.LBool:
|
|
return bool(v), nil
|
|
case lua.LNumber:
|
|
return float64(v), nil
|
|
default:
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
func IsLuaTableArray(L *lua.LState, v *lua.LTable) (bool, error) {
|
|
logger.Trace("Checking if Lua table is an array")
|
|
L.SetGlobal("table_to_check", v)
|
|
|
|
// Use our predefined helper function from InitLuaHelpers
|
|
err := L.DoString(`is_array = isArray(table_to_check)`)
|
|
if err != nil {
|
|
logger.Error("Error determining if table is an array: %v", err)
|
|
return false, fmt.Errorf("error determining if table is array: %w", err)
|
|
}
|
|
|
|
// Check the result of our Lua function
|
|
isArray := L.GetGlobal("is_array")
|
|
// LVIsFalse returns true if a given LValue is a nil or false otherwise false.
|
|
result := !lua.LVIsFalse(isArray)
|
|
logger.Trace("Lua table is array: %v", result)
|
|
return result, nil
|
|
}
|
|
|
|
// InitLuaHelpers initializes common Lua helper functions
|
|
func InitLuaHelpers(L *lua.LState) error {
|
|
logger.Debug("Loading Lua helper functions")
|
|
|
|
helperScript := `
|
|
-- Custom Lua helpers for math operations
|
|
function min(a, b) return math.min(a, b) end
|
|
function max(a, b) return math.max(a, b) end
|
|
function round(x, n)
|
|
if n == nil then n = 0 end
|
|
return math.floor(x * 10^n + 0.5) / 10^n
|
|
end
|
|
function floor(x) return math.floor(x) end
|
|
function ceil(x) return math.ceil(x) end
|
|
function upper(s) return string.upper(s) end
|
|
function lower(s) return string.lower(s) end
|
|
function format(s, ...) return string.format(s, ...) end
|
|
function trim(s) return string.gsub(s, "^%s*(.-)%s*$", "%1") end
|
|
|
|
-- String split helper
|
|
function strsplit(inputstr, sep)
|
|
if sep == nil then
|
|
sep = "%s"
|
|
end
|
|
local t = {}
|
|
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
|
|
table.insert(t, str)
|
|
end
|
|
return t
|
|
end
|
|
|
|
---@param table table
|
|
---@param depth number?
|
|
function DumpTable(table, depth)
|
|
if depth == nil then
|
|
depth = 0
|
|
end
|
|
if (depth > 200) then
|
|
print("Error: Depth > 200 in dumpTable()")
|
|
return
|
|
end
|
|
for k, v in pairs(table) do
|
|
if (type(v) == "table") then
|
|
print(string.rep(" ", depth) .. k .. ":")
|
|
DumpTable(v, depth + 1)
|
|
else
|
|
print(string.rep(" ", depth) .. k .. ": ", v)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- String to number conversion helper
|
|
function num(str)
|
|
return tonumber(str) or 0
|
|
end
|
|
|
|
-- Number to string conversion
|
|
function str(num)
|
|
return tostring(num)
|
|
end
|
|
|
|
-- Check if string is numeric
|
|
function is_number(str)
|
|
return tonumber(str) ~= nil
|
|
end
|
|
|
|
function isArray(t)
|
|
if type(t) ~= "table" then return false end
|
|
local max = 0
|
|
local count = 0
|
|
for k, _ in pairs(t) do
|
|
if type(k) ~= "number" or k < 1 or math.floor(k) ~= k then
|
|
return false
|
|
end
|
|
max = math.max(max, k)
|
|
count = count + 1
|
|
end
|
|
return max == count
|
|
end
|
|
|
|
modified = false
|
|
`
|
|
if err := L.DoString(helperScript); err != nil {
|
|
logger.Error("Failed to load Lua helper functions: %v", err)
|
|
return fmt.Errorf("error loading helper functions: %v", err)
|
|
}
|
|
|
|
logger.Debug("Setting up Lua print function to Go")
|
|
L.SetGlobal("print", L.NewFunction(printToGo))
|
|
L.SetGlobal("fetch", L.NewFunction(fetch))
|
|
return nil
|
|
}
|
|
|
|
// LimitString truncates a string to maxLen and adds "..." if truncated
|
|
func LimitString(s string, maxLen int) string {
|
|
s = strings.ReplaceAll(s, "\n", "\\n")
|
|
if len(s) <= maxLen {
|
|
return s
|
|
}
|
|
return s[:maxLen-3] + "..."
|
|
}
|
|
|
|
func PrependLuaAssignment(luaExpr string) string {
|
|
// Auto-prepend v1 for expressions starting with operators
|
|
if strings.HasPrefix(luaExpr, "*") ||
|
|
strings.HasPrefix(luaExpr, "/") ||
|
|
strings.HasPrefix(luaExpr, "+") ||
|
|
strings.HasPrefix(luaExpr, "-") ||
|
|
strings.HasPrefix(luaExpr, "^") ||
|
|
strings.HasPrefix(luaExpr, "%") {
|
|
luaExpr = "v1 = v1" + luaExpr
|
|
} else if strings.HasPrefix(luaExpr, "=") {
|
|
// Handle direct assignment with = operator
|
|
luaExpr = "v1 " + luaExpr
|
|
}
|
|
|
|
// Add assignment if needed
|
|
if !strings.Contains(luaExpr, "=") {
|
|
luaExpr = "v1 = " + luaExpr
|
|
}
|
|
return luaExpr
|
|
}
|
|
|
|
// BuildLuaScript prepares a Lua expression from shorthand notation
|
|
func BuildLuaScript(luaExpr string) string {
|
|
logger.Debug("Building Lua script from expression: %s", luaExpr)
|
|
|
|
luaExpr = PrependLuaAssignment(luaExpr)
|
|
|
|
// This allows the user to specify whether or not they modified a value
|
|
// If they do nothing we assume they did modify (no return at all)
|
|
// If they return before our return then they themselves specify what they did
|
|
// If nothing is returned lua assumes nil
|
|
// So we can say our value was modified if the return value is either nil or true
|
|
// If the return value is false then the user wants to keep the original
|
|
fullScript := fmt.Sprintf(`
|
|
function run()
|
|
%s
|
|
end
|
|
local res = run()
|
|
modified = res == nil or res
|
|
`, luaExpr)
|
|
|
|
return fullScript
|
|
}
|
|
|
|
func printToGo(L *lua.LState) int {
|
|
top := L.GetTop()
|
|
|
|
args := make([]interface{}, top)
|
|
for i := 1; i <= top; i++ {
|
|
args[i-1] = L.Get(i)
|
|
}
|
|
|
|
// Format the message with proper spacing between arguments
|
|
var parts []string
|
|
for _, arg := range args {
|
|
parts = append(parts, fmt.Sprintf("%v", arg))
|
|
}
|
|
message := strings.Join(parts, " ")
|
|
|
|
// Use the LUA log level with a script tag
|
|
logger.Lua("%s", message)
|
|
return 0
|
|
}
|
|
|
|
func fetch(L *lua.LState) int {
|
|
// Get URL from first argument
|
|
url := L.ToString(1)
|
|
if url == "" {
|
|
L.Push(lua.LNil)
|
|
L.Push(lua.LString("URL is required"))
|
|
return 2
|
|
}
|
|
|
|
// Get options from second argument if provided
|
|
var method string = "GET"
|
|
var headers map[string]string = make(map[string]string)
|
|
var body string = ""
|
|
|
|
if L.GetTop() > 1 {
|
|
options := L.ToTable(2)
|
|
if options != nil {
|
|
// Get method
|
|
if methodVal := options.RawGetString("method"); methodVal != lua.LNil {
|
|
method = methodVal.String()
|
|
}
|
|
|
|
// Get headers
|
|
if headersVal := options.RawGetString("headers"); headersVal != lua.LNil {
|
|
if headersTable, ok := headersVal.(*lua.LTable); ok {
|
|
headersTable.ForEach(func(key lua.LValue, value lua.LValue) {
|
|
headers[key.String()] = value.String()
|
|
})
|
|
}
|
|
}
|
|
|
|
// Get body
|
|
if bodyVal := options.RawGetString("body"); bodyVal != lua.LNil {
|
|
body = bodyVal.String()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create HTTP request
|
|
req, err := http.NewRequest(method, url, strings.NewReader(body))
|
|
if err != nil {
|
|
L.Push(lua.LNil)
|
|
L.Push(lua.LString(fmt.Sprintf("Error creating request: %v", err)))
|
|
return 2
|
|
}
|
|
|
|
// Set headers
|
|
for key, value := range headers {
|
|
req.Header.Set(key, value)
|
|
}
|
|
|
|
// Make request
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
L.Push(lua.LNil)
|
|
L.Push(lua.LString(fmt.Sprintf("Error making request: %v", err)))
|
|
return 2
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Read response body
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
L.Push(lua.LNil)
|
|
L.Push(lua.LString(fmt.Sprintf("Error reading response: %v", err)))
|
|
return 2
|
|
}
|
|
|
|
// Create response table
|
|
responseTable := L.NewTable()
|
|
responseTable.RawSetString("status", lua.LNumber(resp.StatusCode))
|
|
responseTable.RawSetString("statusText", lua.LString(resp.Status))
|
|
responseTable.RawSetString("ok", lua.LBool(resp.StatusCode >= 200 && resp.StatusCode < 300))
|
|
responseTable.RawSetString("body", lua.LString(string(bodyBytes)))
|
|
|
|
// Set headers in response
|
|
headersTable := L.NewTable()
|
|
for key, values := range resp.Header {
|
|
headersTable.RawSetString(key, lua.LString(values[0]))
|
|
}
|
|
responseTable.RawSetString("headers", headersTable)
|
|
|
|
L.Push(responseTable)
|
|
return 1
|
|
}
|