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 }