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 -- 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 }