package processor import ( "fmt" "io" "net/http" "regexp" "strings" "cook/utils" logger "git.site.quack-lab.dev/dave/cylogger" lua "github.com/yuin/gopher-lua" ) // processorLogger is a scoped logger for the processor package. var processorLogger = logger.Default.WithPrefix("processor") // Maybe we make this an interface again for the shits and giggles // We will see, it could easily be... var globalVariables = map[string]interface{}{} func SetVariables(vars map[string]interface{}) { for k, v := range vars { globalVariables[k] = v } } func NewLuaState() (*lua.LState, error) { newLStateLogger := processorLogger.WithPrefix("NewLuaState") newLStateLogger.Debug("Creating new Lua state") L := lua.NewState() // defer L.Close() // Load math library newLStateLogger.Debug("Loading Lua math library") L.Push(L.GetGlobal("require")) L.Push(lua.LString("math")) if err := L.PCall(1, 1, nil); err != nil { newLStateLogger.Error("Failed to load Lua math library: %v", err) return nil, fmt.Errorf("error loading Lua math library: %v", err) } newLStateLogger.Debug("Lua math library loaded") // Initialize helper functions newLStateLogger.Debug("Initializing Lua helper functions") if err := InitLuaHelpers(L); err != nil { newLStateLogger.Error("Failed to initialize Lua helper functions: %v", err) return nil, err } newLStateLogger.Debug("Lua helper functions initialized") // Inject global variables if len(globalVariables) > 0 { newLStateLogger.Debug("Injecting %d global variables into Lua state", len(globalVariables)) for k, v := range globalVariables { switch val := v.(type) { case int: L.SetGlobal(k, lua.LNumber(float64(val))) case int64: L.SetGlobal(k, lua.LNumber(float64(val))) case float32: L.SetGlobal(k, lua.LNumber(float64(val))) case float64: L.SetGlobal(k, lua.LNumber(val)) case string: L.SetGlobal(k, lua.LString(val)) case bool: if val { L.SetGlobal(k, lua.LTrue) } else { L.SetGlobal(k, lua.LFalse) } default: // Fallback to string representation L.SetGlobal(k, lua.LString(fmt.Sprintf("%v", val))) } } } newLStateLogger.Debug("New Lua state created successfully") return L, nil } // FromLua converts a Lua table to a struct or map recursively func FromLua(L *lua.LState, luaValue lua.LValue) (interface{}, error) { fromLuaLogger := processorLogger.WithPrefix("FromLua").WithField("luaType", luaValue.Type().String()) fromLuaLogger.Debug("Converting Lua value to Go interface") switch v := luaValue.(type) { case *lua.LTable: fromLuaLogger.Debug("Processing Lua table") isArray, err := IsLuaTableArray(L, v) if err != nil { fromLuaLogger.Error("Failed to determine if Lua table is array: %v", err) return nil, err } fromLuaLogger.Debug("Lua table is array: %t", isArray) if isArray { fromLuaLogger.Debug("Converting Lua table to Go array") result := make([]interface{}, 0) v.ForEach(func(key lua.LValue, value lua.LValue) { converted, _ := FromLua(L, value) result = append(result, converted) }) fromLuaLogger.Trace("Converted Go array: %v", result) return result, nil } else { fromLuaLogger.Debug("Converting Lua table to Go map") result := make(map[string]interface{}) v.ForEach(func(key lua.LValue, value lua.LValue) { converted, _ := FromLua(L, value) result[key.String()] = converted }) fromLuaLogger.Trace("Converted Go map: %v", result) return result, nil } case lua.LString: fromLuaLogger.Debug("Converting Lua string to Go string") fromLuaLogger.Trace("Lua string: %q", string(v)) return string(v), nil case lua.LBool: fromLuaLogger.Debug("Converting Lua boolean to Go boolean") fromLuaLogger.Trace("Lua boolean: %t", bool(v)) return bool(v), nil case lua.LNumber: fromLuaLogger.Debug("Converting Lua number to Go float64") fromLuaLogger.Trace("Lua number: %f", float64(v)) return float64(v), nil default: fromLuaLogger.Debug("Unsupported Lua type, returning nil") return nil, nil } } func IsLuaTableArray(L *lua.LState, v *lua.LTable) (bool, error) { isLuaTableArrayLogger := processorLogger.WithPrefix("IsLuaTableArray") isLuaTableArrayLogger.Debug("Checking if Lua table is an array") isLuaTableArrayLogger.Trace("Lua table input: %v", v) 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 { isLuaTableArrayLogger.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) isLuaTableArrayLogger.Debug("Lua table is array: %t", result) isLuaTableArrayLogger.Trace("isArray result Lua value: %v", isArray) return result, nil } // InitLuaHelpers initializes common Lua helper functions func InitLuaHelpers(L *lua.LState) error { initLuaHelpersLogger := processorLogger.WithPrefix("InitLuaHelpers") initLuaHelpersLogger.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 { initLuaHelpersLogger.Error("Failed to load Lua helper functions: %v", err) return fmt.Errorf("error loading helper functions: %v", err) } initLuaHelpersLogger.Debug("Lua helper functions loaded") initLuaHelpersLogger.Debug("Setting up Lua print function to Go") L.SetGlobal("print", L.NewFunction(printToGo)) L.SetGlobal("fetch", L.NewFunction(fetch)) initLuaHelpersLogger.Debug("Lua print and fetch functions bound to Go") return nil } func PrependLuaAssignment(luaExpr string) string { prependLuaAssignmentLogger := processorLogger.WithPrefix("PrependLuaAssignment").WithField("originalLuaExpr", luaExpr) prependLuaAssignmentLogger.Debug("Prepending Lua assignment if necessary") // 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 prependLuaAssignmentLogger.Debug("Prepended 'v1 = v1' due to operator prefix") } else if strings.HasPrefix(luaExpr, "=") { // Handle direct assignment with = operator luaExpr = "v1 " + luaExpr prependLuaAssignmentLogger.Debug("Prepended 'v1' due to direct assignment operator") } // Add assignment if needed if !strings.Contains(luaExpr, "=") { luaExpr = "v1 = " + luaExpr prependLuaAssignmentLogger.Debug("Prepended 'v1 =' as no assignment was found") } prependLuaAssignmentLogger.Trace("Final Lua expression after prepending: %q", luaExpr) return luaExpr } // BuildLuaScript prepares a Lua expression from shorthand notation func BuildLuaScript(luaExpr string) string { buildLuaScriptLogger := processorLogger.WithPrefix("BuildLuaScript").WithField("inputLuaExpr", luaExpr) buildLuaScriptLogger.Debug("Building full Lua script from expression") // Perform $var substitutions from globalVariables luaExpr = replaceVariables(luaExpr) luaExpr = PrependLuaAssignment(luaExpr) fullScript := fmt.Sprintf(` function run() %s end local res = run() modified = res == nil or res `, luaExpr) buildLuaScriptLogger.Trace("Generated full Lua script: %q", utils.LimitString(fullScript, 200)) return fullScript } func replaceVariables(expr string) string { // $varName -> literal value varNameRe := regexp.MustCompile(`\$(\w+)`) return varNameRe.ReplaceAllStringFunc(expr, func(m string) string { name := varNameRe.FindStringSubmatch(m)[1] if v, ok := globalVariables[name]; ok { switch val := v.(type) { case int, int64, float32, float64: return fmt.Sprintf("%v", val) case bool: if val { return "true" } else { return "false" } case string: // Quote strings for Lua literal return fmt.Sprintf("%q", val) default: return fmt.Sprintf("%q", fmt.Sprintf("%v", val)) } } return m }) } func printToGo(L *lua.LState) int { printToGoLogger := processorLogger.WithPrefix("printToGo") printToGoLogger.Debug("Lua print function called, redirecting to Go logger") 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, " ") printToGoLogger.Trace("Lua print message: %q", message) // Use the LUA log level with a script tag logger.Lua("%s", message) printToGoLogger.Debug("Message logged from Lua") return 0 } func fetch(L *lua.LState) int { fetchLogger := processorLogger.WithPrefix("fetch") fetchLogger.Debug("Lua fetch function called") // Get URL from first argument url := L.ToString(1) if url == "" { fetchLogger.Error("Fetch failed: URL is required") L.Push(lua.LNil) L.Push(lua.LString("URL is required")) return 2 } fetchLogger.Debug("Fetching URL: %q", url) // 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 { fetchLogger.Debug("Processing fetch options") // Get method if methodVal := options.RawGetString("method"); methodVal != lua.LNil { method = methodVal.String() fetchLogger.Trace("Method from options: %q", method) } // Get headers if headersVal := options.RawGetString("headers"); headersVal != lua.LNil { if headersTable, ok := headersVal.(*lua.LTable); ok { fetchLogger.Trace("Processing headers table") headersTable.ForEach(func(key lua.LValue, value lua.LValue) { headers[key.String()] = value.String() fetchLogger.Trace("Header: %q = %q", key.String(), value.String()) }) } fetchLogger.Trace("All headers: %v", headers) } // Get body if bodyVal := options.RawGetString("body"); bodyVal != lua.LNil { body = bodyVal.String() fetchLogger.Trace("Body from options: %q", utils.LimitString(body, 100)) } } } fetchLogger.Debug("Fetch request details: Method=%q, URL=%q, BodyLength=%d, Headers=%v", method, url, len(body), headers) // Create HTTP request req, err := http.NewRequest(method, url, strings.NewReader(body)) if err != nil { fetchLogger.Error("Error creating HTTP request: %v", err) 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) } fetchLogger.Debug("HTTP request created and headers set") fetchLogger.Trace("HTTP Request: %+v", req) // Make request client := &http.Client{} resp, err := client.Do(req) if err != nil { fetchLogger.Error("Error making HTTP request: %v", err) L.Push(lua.LNil) L.Push(lua.LString(fmt.Sprintf("Error making request: %v", err))) return 2 } defer func() { fetchLogger.Debug("Closing HTTP response body") resp.Body.Close() }() fetchLogger.Debug("HTTP request executed. Status Code: %d", resp.StatusCode) // Read response body bodyBytes, err := io.ReadAll(resp.Body) if err != nil { fetchLogger.Error("Error reading response body: %v", err) L.Push(lua.LNil) L.Push(lua.LString(fmt.Sprintf("Error reading response: %v", err))) return 2 } fetchLogger.Trace("Response body length: %d", len(bodyBytes)) // 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))) fetchLogger.Debug("Created Lua response table") // Set headers in response headersTable := L.NewTable() for key, values := range resp.Header { headersTable.RawSetString(key, lua.LString(values[0])) fetchLogger.Trace("Response header: %q = %q", key, values[0]) } responseTable.RawSetString("headers", headersTable) fetchLogger.Trace("Full response table: %v", responseTable) L.Push(responseTable) fetchLogger.Debug("Pushed response table to Lua stack") return 1 }