Hallucinate some logs

Hallucinate more logs

fix(utils/db.go): handle auto migration errors gracefully

fix(utils/modifycommand.go): improve file matching accuracy

fix(processor/regex.go): improve capture group deduplication logic

fix(utils/db.go): add logging for database wrapper initialization

feat(processor/regex.go): preserve input order when deduplicating capture groups

fix(utils/modifycommand.go): add logging for file matching skips

feat(processor/regex.go): add logging for capture group processing

feat(main.go): add trace logging for arguments and parallel workers

fix(main.go): add trace logging for file content

fix(utils/db.go): add logging for database opening

fix(main.go): add trace logging for file processing

fix(utils/modifycommand.go): improve file matching by using absolute paths

feat(modifycommand.go): add trace logging for file matching in AssociateFilesWithCommands

feat(main.go): add per-file association summary for better visibility when debugging
This commit is contained in:
2025-08-07 23:33:19 +02:00
parent 8ffd8af13c
commit 4b58e00c26
8 changed files with 702 additions and 328 deletions

View File

@@ -6,151 +6,106 @@ import (
"net/http"
"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...
func NewLuaState() (*lua.LState, error) {
newLStateLogger := processorLogger.WithPrefix("NewLuaState")
newLStateLogger.Debug("Creating new Lua state")
L := lua.NewState()
// defer L.Close()
// Load math library
logger.Debug("Loading Lua 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 {
logger.Error("Failed to load Lua math library: %v", err)
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
logger.Debug("Initializing Lua helper functions")
newLStateLogger.Debug("Initializing Lua helper functions")
if err := InitLuaHelpers(L); err != nil {
logger.Error("Failed to initialize Lua helper functions: %v", err)
newLStateLogger.Error("Failed to initialize Lua helper functions: %v", err)
return nil, err
}
newLStateLogger.Debug("Lua helper functions initialized")
newLStateLogger.Debug("New Lua state created successfully")
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) {
fromLuaLogger := processorLogger.WithPrefix("FromLua").WithField("luaType", luaValue.Type().String())
fromLuaLogger.Debug("Converting Lua value to Go interface")
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:
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) {
logger.Trace("Checking if Lua table is an array")
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 {
logger.Error("Error determining if table is an array: %v", err)
isLuaTableArrayLogger.Error("Error determining if table is an array: %v", err)
return false, fmt.Errorf("error determining if table is array: %w", err)
}
@@ -158,13 +113,15 @@ func IsLuaTableArray(L *lua.LState, v *lua.LTable) (bool, error) {
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)
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 {
logger.Debug("Loading Lua helper functions")
initLuaHelpersLogger := processorLogger.WithPrefix("InitLuaHelpers")
initLuaHelpersLogger.Debug("Loading Lua helper functions")
helperScript := `
-- Custom Lua helpers for math operations
@@ -245,26 +202,21 @@ end
modified = false
`
if err := L.DoString(helperScript); err != nil {
logger.Error("Failed to load Lua helper functions: %v", err)
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")
logger.Debug("Setting up Lua print function to Go")
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
}
// 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 {
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, "/") ||
@@ -273,30 +225,29 @@ func PrependLuaAssignment(luaExpr string) string {
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 {
logger.Debug("Building Lua script from expression: %s", luaExpr)
buildLuaScriptLogger := processorLogger.WithPrefix("BuildLuaScript").WithField("inputLuaExpr", luaExpr)
buildLuaScriptLogger.Debug("Building full Lua script from expression")
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
@@ -304,11 +255,14 @@ func BuildLuaScript(luaExpr string) string {
local res = run()
modified = res == nil or res
`, luaExpr)
buildLuaScriptLogger.Trace("Generated full Lua script: %q", utils.LimitString(fullScript, 200))
return fullScript
}
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)
@@ -322,20 +276,26 @@ func printToGo(L *lua.LState) int {
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"
@@ -345,30 +305,38 @@ func fetch(L *lua.LState) int {
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
@@ -378,24 +346,33 @@ func fetch(L *lua.LState) int {
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 resp.Body.Close()
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()
@@ -403,14 +380,18 @@ func fetch(L *lua.LState) int {
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
}

View File

@@ -12,6 +12,9 @@ import (
lua "github.com/yuin/gopher-lua"
)
// regexLogger is a scoped logger for the processor/regex package.
var regexLogger = logger.Default.WithPrefix("processor/regex")
type CaptureGroup struct {
Name string
Value string
@@ -23,52 +26,59 @@ type CaptureGroup struct {
// The filename here exists ONLY so we can pass it to the lua environment
// It's not used for anything else
func ProcessRegex(content string, command utils.ModifyCommand, filename string) ([]utils.ReplaceCommand, error) {
var commands []utils.ReplaceCommand
logger.Trace("Processing regex: %q", command.Regex)
processRegexLogger := regexLogger.WithPrefix("ProcessRegex").WithField("commandName", command.Name).WithField("file", filename)
processRegexLogger.Debug("Starting regex processing for file")
processRegexLogger.Trace("Initial file content length: %d", len(content))
processRegexLogger.Trace("Command details: %+v", command)
var commands []utils.ReplaceCommand
// Start timing the regex processing
startTime := time.Now()
// We don't HAVE to do this multiple times for a pattern
// But it's quick enough for us to not care
pattern := resolveRegexPlaceholders(command.Regex)
processRegexLogger.Debug("Resolved regex placeholders. Pattern: %s", pattern)
// I'm not too happy about having to trim regex, we could have meaningful whitespace or newlines
// But it's a compromise that allows us to use | in yaml
// Otherwise we would have to escape every god damn pair of quotation marks
// And a bunch of other shit
pattern = strings.TrimSpace(pattern)
logger.Debug("Compiling regex pattern: %s", pattern)
processRegexLogger.Debug("Trimmed regex pattern: %s", pattern)
patternCompileStart := time.Now()
compiledPattern, err := regexp.Compile(pattern)
if err != nil {
logger.Error("Error compiling pattern: %v", err)
processRegexLogger.Error("Error compiling pattern %q: %v", pattern, err)
return commands, fmt.Errorf("error compiling pattern: %v", err)
}
logger.Debug("Compiled pattern successfully in %v: %s", time.Since(patternCompileStart), pattern)
processRegexLogger.Debug("Compiled pattern successfully in %v", time.Since(patternCompileStart))
// Same here, it's just string concatenation, it won't kill us
// More important is that we don't fuck up the command
// But we shouldn't be able to since it's passed by value
previous := command.Lua
previousLuaExpr := command.Lua
luaExpr := BuildLuaScript(command.Lua)
logger.Debug("Transformed Lua expression: %q → %q", previous, luaExpr)
processRegexLogger.Debug("Transformed Lua expression: %q → %q", previousLuaExpr, luaExpr)
processRegexLogger.Trace("Full Lua script: %q", utils.LimitString(luaExpr, 200))
// Process all regex matches
matchFindStart := time.Now()
indices := compiledPattern.FindAllStringSubmatchIndex(content, -1)
matchFindDuration := time.Since(matchFindStart)
logger.Debug("Found %d matches in content of length %d (search took %v)",
processRegexLogger.Debug("Found %d matches in content of length %d (search took %v)",
len(indices), len(content), matchFindDuration)
processRegexLogger.Trace("Match indices: %v", indices)
// Log pattern complexity metrics
patternComplexity := estimatePatternComplexity(pattern)
logger.Debug("Pattern complexity estimate: %d", patternComplexity)
processRegexLogger.Debug("Pattern complexity estimate: %d", patternComplexity)
if len(indices) == 0 {
logger.Warning("No matches found for regex: %q", pattern)
logger.Debug("Total regex processing time: %v", time.Since(startTime))
processRegexLogger.Warning("No matches found for regex: %q", pattern)
processRegexLogger.Debug("Total regex processing time: %v", time.Since(startTime))
return commands, nil
}
@@ -77,12 +87,13 @@ func ProcessRegex(content string, command utils.ModifyCommand, filename string)
// By going backwards we fuck up all the indices to the end of the file that we don't care about
// Because there either aren't any (last match) or they're already modified (subsequent matches)
for i, matchIndices := range indices {
logger.Debug("Processing match %d of %d", i+1, len(indices))
logger.Trace("Match indices: %v (match position %d-%d)", matchIndices, matchIndices[0], matchIndices[1])
matchLogger := processRegexLogger.WithField("matchNum", i+1)
matchLogger.Debug("Processing match %d of %d", i+1, len(indices))
matchLogger.Trace("Match indices: %v (match position %d-%d)", matchIndices, matchIndices[0], matchIndices[1])
L, err := NewLuaState()
if err != nil {
logger.Error("Error creating Lua state: %v", err)
matchLogger.Error("Error creating Lua state: %v", err)
return commands, fmt.Errorf("error creating Lua state: %v", err)
}
L.SetGlobal("file", lua.LString(filename))
@@ -90,7 +101,7 @@ func ProcessRegex(content string, command utils.ModifyCommand, filename string)
// Maybe we want to close them every iteration
// We'll leave it as is for now
defer L.Close()
logger.Trace("Lua state created successfully for match %d", i+1)
matchLogger.Trace("Lua state created successfully for match %d", i+1)
// Why we're doing this whole song and dance of indices is to properly handle empty matches
// Plus it's a little cleaner to surgically replace our matches
@@ -99,20 +110,17 @@ func ProcessRegex(content string, command utils.ModifyCommand, filename string)
// So when we're cutting open the array we say 0:7 + modified + 7:end
// As if concatenating in the middle of the array
// Plus it supports lookarounds
match := content[matchIndices[0]:matchIndices[1]]
matchPreview := match
if len(match) > 50 {
matchPreview = match[:47] + "..."
}
logger.Trace("Matched content: %q (length: %d)", matchPreview, len(match))
matchContent := content[matchIndices[0]:matchIndices[1]]
matchPreview := utils.LimitString(matchContent, 50)
matchLogger.Trace("Matched content: %q (length: %d)", matchPreview, len(matchContent))
groups := matchIndices[2:]
if len(groups) <= 0 {
logger.Warning("No capture groups found for match %q and regex %q", matchPreview, pattern)
matchLogger.Warning("No capture groups found for match %q and regex %q", matchPreview, pattern)
continue
}
if len(groups)%2 == 1 {
logger.Warning("Invalid number of group indices (%d), should be even: %v", len(groups), groups)
matchLogger.Warning("Invalid number of group indices (%d), should be even: %v", len(groups), groups)
continue
}
@@ -123,11 +131,11 @@ func ProcessRegex(content string, command utils.ModifyCommand, filename string)
validGroups++
}
}
logger.Debug("Found %d valid capture groups in match", validGroups)
matchLogger.Debug("Found %d valid capture groups in match", validGroups)
for _, index := range groups {
if index == -1 {
logger.Warning("Negative index encountered in match indices %v. This may indicate an issue with the regex pattern or an empty/optional capture group.", matchIndices)
matchLogger.Warning("Negative index encountered in match indices %v. This may indicate an issue with the regex pattern or an empty/optional capture group.", matchIndices)
continue
}
}
@@ -142,6 +150,7 @@ func ProcessRegex(content string, command utils.ModifyCommand, filename string)
start := groups[i*2]
end := groups[i*2+1]
if start == -1 || end == -1 {
matchLogger.Debug("Skipping empty or unmatched capture group #%d (name: %q)", i+1, name)
continue
}
@@ -154,75 +163,80 @@ func ProcessRegex(content string, command utils.ModifyCommand, filename string)
// Include name info in log if available
if name != "" {
logger.Trace("Capture group '%s': %q (pos %d-%d)", name, value, start, end)
matchLogger.Trace("Capture group '%s': %q (pos %d-%d)", name, value, start, end)
} else {
logger.Trace("Capture group #%d: %q (pos %d-%d)", i+1, value, start, end)
matchLogger.Trace("Capture group #%d: %q (pos %d-%d)", i+1, value, start, end)
}
}
// Use the DeduplicateGroups flag to control whether to deduplicate capture groups
if !command.NoDedup {
logger.Debug("Deduplicating capture groups as specified in command settings")
matchLogger.Debug("Deduplicating capture groups as specified in command settings")
captureGroups = deduplicateGroups(captureGroups)
matchLogger.Trace("Capture groups after deduplication: %v", captureGroups)
} else {
matchLogger.Debug("Skipping deduplication of capture groups (NoDedup is true)")
}
if err := toLua(L, captureGroups); err != nil {
logger.Error("Failed to set Lua variables: %v", err)
matchLogger.Error("Failed to set Lua variables for capture groups: %v", err)
continue
}
logger.Trace("Set %d capture groups as Lua variables", len(captureGroups))
matchLogger.Debug("Set %d capture groups as Lua variables", len(captureGroups))
matchLogger.Trace("Lua globals set for capture groups")
if err := L.DoString(luaExpr); err != nil {
logger.Error("Lua script execution failed: %v\nScript: %s\nCapture Groups: %+v",
err, luaExpr, captureGroups)
matchLogger.Error("Lua script execution failed: %v\nScript: %s\nCapture Groups: %+v",
err, utils.LimitString(luaExpr, 200), captureGroups)
continue
}
logger.Trace("Lua script executed successfully")
matchLogger.Debug("Lua script executed successfully")
// Get modifications from Lua
captureGroups, err = fromLua(L, captureGroups)
updatedCaptureGroups, err := fromLua(L, captureGroups)
if err != nil {
logger.Error("Failed to retrieve modifications from Lua: %v", err)
matchLogger.Error("Failed to retrieve modifications from Lua: %v", err)
continue
}
logger.Trace("Retrieved updated values from Lua")
matchLogger.Debug("Retrieved updated values from Lua")
matchLogger.Trace("Updated capture groups from Lua: %v", updatedCaptureGroups)
replacement := ""
replacementVar := L.GetGlobal("replacement")
if replacementVar.Type() != lua.LTNil {
replacement = replacementVar.String()
logger.Debug("Using global replacement: %q", replacement)
matchLogger.Debug("Using global replacement variable from Lua: %q", replacement)
}
// Check if modification flag is set
modifiedVal := L.GetGlobal("modified")
if modifiedVal.Type() != lua.LTBool || !lua.LVAsBool(modifiedVal) {
logger.Debug("Skipping match - no modifications made by Lua script")
matchLogger.Debug("Skipping match - no modifications indicated by Lua script")
continue
}
if replacement == "" {
// Apply the modifications to the original match
replacement = match
replacement = matchContent
// Count groups that were actually modified
modifiedGroups := 0
for _, capture := range captureGroups {
modifiedGroupsCount := 0
for _, capture := range updatedCaptureGroups {
if capture.Value != capture.Updated {
modifiedGroups++
modifiedGroupsCount++
}
}
logger.Info("%d of %d capture groups identified for modification", modifiedGroups, len(captureGroups))
matchLogger.Info("%d of %d capture groups identified for modification", modifiedGroupsCount, len(updatedCaptureGroups))
for _, capture := range captureGroups {
for _, capture := range updatedCaptureGroups {
if capture.Value == capture.Updated {
logger.Info("Capture group unchanged: %s", LimitString(capture.Value, 50))
matchLogger.Debug("Capture group unchanged: %s", utils.LimitString(capture.Value, 50))
continue
}
// Log what changed with context
logger.Debug("Capture group %s scheduled for modification: %q → %q",
capture.Name, capture.Value, capture.Updated)
matchLogger.Debug("Capture group %q scheduled for modification: %q → %q",
capture.Name, utils.LimitString(capture.Value, 50), utils.LimitString(capture.Updated, 50))
// Indices of the group are relative to content
// To relate them to match we have to subtract the match start index
@@ -232,42 +246,57 @@ func ProcessRegex(content string, command utils.ModifyCommand, filename string)
To: capture.Range[1],
With: capture.Updated,
})
matchLogger.Trace("Added replacement command: %+v", commands[len(commands)-1])
}
} else {
matchLogger.Debug("Using full replacement string from Lua: %q", utils.LimitString(replacement, 50))
commands = append(commands, utils.ReplaceCommand{
From: matchIndices[0],
To: matchIndices[1],
With: replacement,
})
matchLogger.Trace("Added full replacement command: %+v", commands[len(commands)-1])
}
}
logger.Debug("Total regex processing time: %v", time.Since(startTime))
processRegexLogger.Debug("Total regex processing time: %v", time.Since(startTime))
processRegexLogger.Debug("Generated %d total modifications", len(commands))
return commands, nil
}
func deduplicateGroups(captureGroups []*CaptureGroup) []*CaptureGroup {
deduplicatedGroups := make([]*CaptureGroup, 0)
deduplicateGroupsLogger := regexLogger.WithPrefix("deduplicateGroups")
deduplicateGroupsLogger.Debug("Starting deduplication of capture groups")
deduplicateGroupsLogger.Trace("Input capture groups: %v", captureGroups)
// Preserve input order and drop any group that overlaps with an already accepted group
accepted := make([]*CaptureGroup, 0, len(captureGroups))
for _, group := range captureGroups {
groupLogger := deduplicateGroupsLogger.WithField("groupName", group.Name).WithField("groupRange", group.Range)
groupLogger.Debug("Processing capture group")
overlaps := false
logger.Debug("Checking capture group: %s with range %v", group.Name, group.Range)
for _, existingGroup := range deduplicatedGroups {
logger.Debug("Comparing with existing group: %s with range %v", existingGroup.Name, existingGroup.Range)
if group.Range[0] < existingGroup.Range[1] && group.Range[1] > existingGroup.Range[0] {
for _, kept := range accepted {
// Overlap if start < keptEnd and end > keptStart (adjacent is allowed)
if group.Range[0] < kept.Range[1] && group.Range[1] > kept.Range[0] {
overlaps = true
logger.Warning("Detected overlap between capture group '%s' and existing group '%s' in range %v-%v and %v-%v", group.Name, existingGroup.Name, group.Range[0], group.Range[1], existingGroup.Range[0], existingGroup.Range[1])
break
}
}
if overlaps {
// We CAN just continue despite this fuckup
logger.Warning("Overlapping capture group: %s", group.Name)
groupLogger.Warning("Overlapping capture group detected and skipped.")
continue
}
logger.Debug("No overlap detected for capture group: %s. Adding to deduplicated groups.", group.Name)
deduplicatedGroups = append(deduplicatedGroups, group)
groupLogger.Debug("Capture group does not overlap with previously accepted groups. Adding.")
accepted = append(accepted, group)
}
return deduplicatedGroups
deduplicateGroupsLogger.Debug("Finished deduplication. Original %d groups, %d deduplicated.", len(captureGroups), len(accepted))
deduplicateGroupsLogger.Trace("Deduplicated groups: %v", accepted)
return accepted
}
// The order of these replaces is important
@@ -276,105 +305,183 @@ func deduplicateGroups(captureGroups []*CaptureGroup) []*CaptureGroup {
// Expand to another capture group in the capture group
// We really only want one (our named) capture group
func resolveRegexPlaceholders(pattern string) string {
resolveLogger := regexLogger.WithPrefix("resolveRegexPlaceholders").WithField("originalPattern", utils.LimitString(pattern, 100))
resolveLogger.Debug("Resolving regex placeholders in pattern")
// Handle special pattern modifications
if !strings.HasPrefix(pattern, "(?s)") {
pattern = "(?s)" + pattern
resolveLogger.Debug("Prepended '(?s)' to pattern for single-line mode")
}
namedGroupNum := regexp.MustCompile(`(?:(\?<[^>]+>)(!num))`)
pattern = namedGroupNum.ReplaceAllStringFunc(pattern, func(match string) string {
funcLogger := resolveLogger.WithPrefix("namedGroupNumReplace").WithField("match", utils.LimitString(match, 50))
funcLogger.Debug("Processing named group !num placeholder")
parts := namedGroupNum.FindStringSubmatch(match)
if len(parts) != 3 {
funcLogger.Warning("Unexpected number of submatches for namedGroupNum: %d. Returning original match.", len(parts))
return match
}
replacement := `-?\d*\.?\d+`
funcLogger.Trace("Replacing !num in named group with: %q", replacement)
return parts[1] + replacement
})
resolveLogger.Debug("Handled named group !num placeholders")
pattern = strings.ReplaceAll(pattern, "!num", `(-?\d*\.?\d+)`)
resolveLogger.Debug("Replaced !num with numeric capture group")
pattern = strings.ReplaceAll(pattern, "!any", `.*?`)
resolveLogger.Debug("Replaced !any with non-greedy wildcard")
repPattern := regexp.MustCompile(`!rep\(([^,]+),\s*(\d+)\)`)
// !rep(pattern, count) repeats the pattern n times
// Inserting !any between each repetition
pattern = repPattern.ReplaceAllStringFunc(pattern, func(match string) string {
funcLogger := resolveLogger.WithPrefix("repPatternReplace").WithField("match", utils.LimitString(match, 50))
funcLogger.Debug("Processing !rep placeholder")
parts := repPattern.FindStringSubmatch(match)
if len(parts) != 3 {
funcLogger.Warning("Unexpected number of submatches for repPattern: %d. Returning original match.", len(parts))
return match
}
repeatedPattern := parts[1]
count := parts[2]
repetitions, _ := strconv.Atoi(count)
return strings.Repeat(repeatedPattern+".*?", repetitions-1) + repeatedPattern
countStr := parts[2]
repetitions, err := strconv.Atoi(countStr)
if err != nil {
funcLogger.Error("Failed to parse repetition count %q: %v. Returning original match.", countStr, err)
return match
}
var finalReplacement string
if repetitions > 0 {
finalReplacement = strings.Repeat(repeatedPattern+".*?", repetitions-1) + repeatedPattern
} else {
finalReplacement = ""
}
funcLogger.Trace("Replaced !rep with %d repetitions of %q: %q", repetitions, utils.LimitString(repeatedPattern, 30), utils.LimitString(finalReplacement, 100))
return finalReplacement
})
resolveLogger.Debug("Handled !rep placeholders")
resolveLogger.Debug("Finished resolving regex placeholders")
resolveLogger.Trace("Final resolved pattern: %q", utils.LimitString(pattern, 100))
return pattern
}
// ToLua sets capture groups as Lua variables (v1, v2, etc. for numeric values and s1, s2, etc. for strings)
func toLua(L *lua.LState, data interface{}) error {
toLuaLogger := regexLogger.WithPrefix("toLua")
toLuaLogger.Debug("Setting capture groups as Lua variables")
captureGroups, ok := data.([]*CaptureGroup)
if !ok {
toLuaLogger.Error("Invalid data type for toLua. Expected []*CaptureGroup, got %T", data)
return fmt.Errorf("expected []*CaptureGroup for captures, got %T", data)
}
toLuaLogger.Trace("Input capture groups: %v", captureGroups)
groupindex := 0
for _, capture := range captureGroups {
groupLogger := toLuaLogger.WithField("captureGroup", capture.Name).WithField("value", utils.LimitString(capture.Value, 50))
groupLogger.Debug("Processing capture group for Lua")
if capture.Name == "" {
// We don't want to change the name of the capture group
// Even if it's empty
tempName := fmt.Sprintf("%d", groupindex+1)
groupindex++
groupLogger.Debug("Unnamed capture group, assigning temporary name: %q", tempName)
L.SetGlobal("s"+tempName, lua.LString(capture.Value))
groupLogger.Trace("Set Lua global s%s = %q", tempName, capture.Value)
val, err := strconv.ParseFloat(capture.Value, 64)
if err == nil {
L.SetGlobal("v"+tempName, lua.LNumber(val))
groupLogger.Trace("Set Lua global v%s = %f", tempName, val)
} else {
groupLogger.Trace("Value %q is not numeric, skipping v%s assignment", capture.Value, tempName)
}
} else {
val, err := strconv.ParseFloat(capture.Value, 64)
if err == nil {
L.SetGlobal(capture.Name, lua.LNumber(val))
groupLogger.Trace("Set Lua global %s = %f (numeric)", capture.Name, val)
} else {
L.SetGlobal(capture.Name, lua.LString(capture.Value))
groupLogger.Trace("Set Lua global %s = %q (string)", capture.Name, capture.Value)
}
}
}
toLuaLogger.Debug("Finished setting capture groups as Lua variables")
return nil
}
// FromLua implements the Processor interface for RegexProcessor
func fromLua(L *lua.LState, captureGroups []*CaptureGroup) ([]*CaptureGroup, error) {
fromLuaLogger := regexLogger.WithPrefix("fromLua")
fromLuaLogger.Debug("Retrieving modifications from Lua for capture groups")
fromLuaLogger.Trace("Initial capture groups: %v", captureGroups)
captureIndex := 0
for _, capture := range captureGroups {
if capture.Name == "" {
capture.Name = fmt.Sprintf("%d", captureIndex+1)
groupLogger := fromLuaLogger.WithField("originalCaptureName", capture.Name).WithField("originalValue", utils.LimitString(capture.Value, 50))
groupLogger.Debug("Processing capture group to retrieve updated value")
vVarName := fmt.Sprintf("v%s", capture.Name)
sVarName := fmt.Sprintf("s%s", capture.Name)
if capture.Name == "" {
// This case means it was an unnamed capture group originally.
// We need to reconstruct the original temporary name to fetch its updated value.
// The name will be set to an integer if it was empty, then incremented.
// So, we use the captureIndex to get the correct 'vX' and 'sX' variables.
tempName := fmt.Sprintf("%d", captureIndex+1)
groupLogger.Debug("Retrieving updated value for unnamed group (temp name: %q)", tempName)
vVarName := fmt.Sprintf("v%s", tempName)
sVarName := fmt.Sprintf("s%s", tempName)
captureIndex++
vLuaVal := L.GetGlobal(vVarName)
sLuaVal := L.GetGlobal(sVarName)
groupLogger.Trace("Lua values for unnamed group: v=%v, s=%v", vLuaVal, sLuaVal)
if sLuaVal.Type() == lua.LTString {
capture.Updated = sLuaVal.String()
groupLogger.Trace("Updated value from s%s (string): %q", tempName, capture.Updated)
}
// Numbers have priority
if vLuaVal.Type() == lua.LTNumber {
capture.Updated = vLuaVal.String()
groupLogger.Trace("Updated value from v%s (numeric): %q", tempName, capture.Updated)
}
} else {
// Easy shit
capture.Updated = L.GetGlobal(capture.Name).String()
// Easy shit, directly use the named capture group
updatedValue := L.GetGlobal(capture.Name)
if updatedValue.Type() != lua.LTNil {
capture.Updated = updatedValue.String()
groupLogger.Trace("Updated value for named group %q: %q", capture.Name, capture.Updated)
} else {
groupLogger.Debug("Named capture group %q not found in Lua globals or is nil. Keeping original value.", capture.Name)
capture.Updated = capture.Value // Keep original if not found or nil
}
}
groupLogger.Debug("Finished processing capture group. Original: %q, Updated: %q", utils.LimitString(capture.Value, 50), utils.LimitString(capture.Updated, 50))
}
fromLuaLogger.Debug("Finished retrieving modifications from Lua")
fromLuaLogger.Trace("Final updated capture groups: %v", captureGroups)
return captureGroups, nil
}
// estimatePatternComplexity gives a rough estimate of regex pattern complexity
// This can help identify potentially problematic patterns
func estimatePatternComplexity(pattern string) int {
estimateComplexityLogger := regexLogger.WithPrefix("estimatePatternComplexity").WithField("pattern", utils.LimitString(pattern, 100))
estimateComplexityLogger.Debug("Estimating regex pattern complexity")
complexity := len(pattern)
// Add complexity for potentially expensive operations
@@ -387,5 +494,6 @@ func estimatePatternComplexity(pattern string) int {
complexity += strings.Count(pattern, "\\1") * 3 // Backreferences
complexity += strings.Count(pattern, "{") * 2 // Counted repetition
estimateComplexityLogger.Debug("Estimated pattern complexity: %d", complexity)
return complexity
}