package processor import ( "crypto/md5" "fmt" "os" "path/filepath" "strings" "time" "github.com/antchfx/xmlquery" lua "github.com/yuin/gopher-lua" "modify/logger" ) // Processor defines the interface for all file processors type Processor interface { // Process handles processing a file with the given pattern and Lua expression // Now implemented as a base function in processor.go // Process(filename string, pattern string, luaExpr string) (int, int, error) // ProcessContent handles processing a string content directly with the given pattern and Lua expression // Returns the modified content, modification count, match count, and any error ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) // ToLua converts processor-specific data to Lua variables ToLua(L *lua.LState, data interface{}) error // FromLua retrieves modified data from Lua FromLua(L *lua.LState) (interface{}, error) } // ModificationRecord tracks a single value modification type ModificationRecord struct { File string OldValue string NewValue string Operation string Context string } 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(p Processor, 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 with %s processor", getProcessorType(p)) modifiedContent, modCount, matchCount, err := p.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 } // Helper function to get a short MD5 hash of content for logging func md5sum(data []byte) []byte { h := md5.New() h.Write(data) return h.Sum(nil)[:4] // Just use first 4 bytes for brevity } // Helper function to detect basic file type from extension and content func detectFileType(filename string, content string) string { ext := strings.ToLower(filepath.Ext(filename)) switch ext { case ".xml": return "XML" case ".json": return "JSON" case ".html", ".htm": return "HTML" case ".txt": return "Text" case ".go": return "Go" case ".js": return "JavaScript" case ".py": return "Python" case ".java": return "Java" case ".c", ".cpp", ".h": return "C/C++" default: // Try content-based detection for common formats if strings.HasPrefix(strings.TrimSpace(content), " 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)) return nil } // Helper utility functions // 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 } // Max returns the maximum of two integers func Max(a, b int) int { if a > b { return a } return b } // Min returns the minimum of two integers func Min(a, b int) int { if a < b { return a } return b }