480 lines
13 KiB
Go
480 lines
13 KiB
Go
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), "<?xml") {
|
|
return "XML"
|
|
}
|
|
if strings.HasPrefix(strings.TrimSpace(content), "{") ||
|
|
strings.HasPrefix(strings.TrimSpace(content), "[") {
|
|
return "JSON"
|
|
}
|
|
if strings.HasPrefix(strings.TrimSpace(content), "<!DOCTYPE html") ||
|
|
strings.HasPrefix(strings.TrimSpace(content), "<html") {
|
|
return "HTML"
|
|
}
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// Helper function to get processor type name
|
|
func getProcessorType(p Processor) string {
|
|
switch p.(type) {
|
|
case *RegexProcessor:
|
|
return "Regex"
|
|
case *XMLProcessor:
|
|
return "XML"
|
|
case *JSONProcessor:
|
|
return "JSON"
|
|
default:
|
|
return "Unknown"
|
|
}
|
|
}
|
|
|
|
// ToLua converts a struct or map to a Lua table recursively
|
|
func ToLua(L *lua.LState, data interface{}) (lua.LValue, error) {
|
|
switch v := data.(type) {
|
|
case *xmlquery.Node:
|
|
luaTable := L.NewTable()
|
|
luaTable.RawSetString("text", lua.LString(v.Data))
|
|
// Should be a map, simple key value pairs
|
|
attr, err := ToLua(L, v.Attr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
luaTable.RawSetString("attr", attr)
|
|
return luaTable, nil
|
|
case map[string]interface{}:
|
|
luaTable := L.NewTable()
|
|
for key, value := range v {
|
|
luaValue, err := ToLua(L, value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
luaTable.RawSetString(key, luaValue)
|
|
}
|
|
return luaTable, nil
|
|
case []interface{}:
|
|
luaTable := L.NewTable()
|
|
for i, value := range v {
|
|
luaValue, err := ToLua(L, value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
luaTable.RawSetInt(i+1, luaValue) // Lua arrays are 1-indexed
|
|
}
|
|
return luaTable, nil
|
|
case string:
|
|
return lua.LString(v), nil
|
|
case bool:
|
|
return lua.LBool(v), nil
|
|
case float64:
|
|
return lua.LNumber(v), nil
|
|
case nil:
|
|
return lua.LNil, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported data type: %T", data)
|
|
}
|
|
}
|
|
|
|
// 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))
|
|
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
|
|
}
|