package processor import ( "encoding/json" "fmt" "os" "path/filepath" "regexp" "strconv" "strings" lua "github.com/yuin/gopher-lua" ) // JSONProcessor implements the Processor interface using JSONPath type JSONProcessor struct { Logger Logger } // NewJSONProcessor creates a new JSONProcessor func NewJSONProcessor(logger Logger) *JSONProcessor { return &JSONProcessor{ Logger: logger, } } // Process implements the Processor interface for JSONProcessor func (p *JSONProcessor) Process(filename string, pattern string, luaExpr string, originalExpr string) (int, int, error) { // Use pattern as JSONPath expression jsonPathExpr := pattern // Read file content fullPath := filepath.Join(".", filename) content, err := os.ReadFile(fullPath) if err != nil { return 0, 0, fmt.Errorf("error reading file: %v", err) } fileContent := string(content) if p.Logger != nil { p.Logger.Printf("File %s loaded: %d bytes", fullPath, len(content)) } // Process the content modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, jsonPathExpr, luaExpr, originalExpr) if err != nil { return 0, 0, err } // If we made modifications, save the file if modCount > 0 { err = os.WriteFile(fullPath, []byte(modifiedContent), 0644) if err != nil { return 0, 0, fmt.Errorf("error writing file: %v", err) } if p.Logger != nil { p.Logger.Printf("Made %d JSON value modifications to %s and saved (%d bytes)", modCount, fullPath, len(modifiedContent)) } } else if p.Logger != nil { p.Logger.Printf("No modifications made to %s", fullPath) } return modCount, matchCount, nil } // ToLua implements the Processor interface for JSONProcessor func (p *JSONProcessor) ToLua(L *lua.LState, data interface{}) error { // For JSON, convert different types to appropriate Lua types return nil } // FromLua implements the Processor interface for JSONProcessor func (p *JSONProcessor) FromLua(L *lua.LState) (interface{}, error) { // Extract changes from Lua environment return nil, nil } // ProcessContent implements the Processor interface for JSONProcessor // It processes JSON content directly without file I/O func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr string, originalExpr string) (string, int, int, error) { // Parse JSON var jsonDoc interface{} err := json.Unmarshal([]byte(content), &jsonDoc) if err != nil { return "", 0, 0, fmt.Errorf("error parsing JSON: %v", err) } // Log the JSONPath expression we're using if p.Logger != nil { p.Logger.Printf("JSON mode selected with JSONPath expression: %s", pattern) } // Initialize Lua state L := lua.NewState() defer L.Close() // Setup Lua helper functions if err := InitLuaHelpers(L); err != nil { return "", 0, 0, err } // Setup JSON helpers p.SetupJSONHelpers(L) // Find matching nodes with simple JSONPath implementation matchingPaths, err := p.findNodePaths(jsonDoc, pattern) if err != nil { return "", 0, 0, fmt.Errorf("error finding JSON nodes: %v", err) } if len(matchingPaths) == 0 { if p.Logger != nil { p.Logger.Printf("No JSON nodes matched JSONPath expression: %s", pattern) } return content, 0, 0, nil } if p.Logger != nil { p.Logger.Printf("Found %d JSON nodes matching the path", len(matchingPaths)) } // Process each node matchCount := len(matchingPaths) modificationCount := 0 modifications := []ModificationRecord{} // Clone the document for modification var modifiedDoc interface{} modifiedBytes, err := json.Marshal(jsonDoc) if err != nil { return "", 0, 0, fmt.Errorf("error cloning JSON document: %v", err) } err = json.Unmarshal(modifiedBytes, &modifiedDoc) if err != nil { return "", 0, 0, fmt.Errorf("error cloning JSON document: %v", err) } // For each matching path, extract value, apply Lua script, and update for i, path := range matchingPaths { // Extract the original value originalValue, err := p.getValueAtPath(jsonDoc, path) if err != nil || originalValue == nil { if p.Logger != nil { p.Logger.Printf("Error getting value at path %v: %v", path, err) } continue } if p.Logger != nil { p.Logger.Printf("Processing node #%d at path %v with value: %v", i+1, path, originalValue) } // Process based on the value type switch val := originalValue.(type) { case float64: // Set up Lua environment for numeric value L.SetGlobal("v1", lua.LNumber(val)) L.SetGlobal("s1", lua.LString(fmt.Sprintf("%v", val))) // Execute Lua script if err := L.DoString(luaExpr); err != nil { if p.Logger != nil { p.Logger.Printf("Lua execution failed for node #%d: %v", i+1, err) } continue } // Extract modified value modVal := L.GetGlobal("v1") if v, ok := modVal.(lua.LNumber); ok { newValue := float64(v) // Update the value in the document only if it changed if newValue != val { err := p.setValueAtPath(modifiedDoc, path, newValue) if err != nil { if p.Logger != nil { p.Logger.Printf("Error updating value at path %v: %v", path, err) } continue } modificationCount++ modifications = append(modifications, ModificationRecord{ File: "", OldValue: fmt.Sprintf("%v", val), NewValue: fmt.Sprintf("%v", newValue), Operation: originalExpr, Context: fmt.Sprintf("(JSONPath: %s)", pattern), }) if p.Logger != nil { p.Logger.Printf("Modified numeric node #%d: %v -> %v", i+1, val, newValue) } } } case string: // Set up Lua environment for string value L.SetGlobal("s1", lua.LString(val)) // Try to convert to number if possible if floatVal, err := strconv.ParseFloat(val, 64); err == nil { L.SetGlobal("v1", lua.LNumber(floatVal)) } else { L.SetGlobal("v1", lua.LNumber(0)) // Default to 0 if not numeric } // Execute Lua script if err := L.DoString(luaExpr); err != nil { if p.Logger != nil { p.Logger.Printf("Lua execution failed for node #%d: %v", i+1, err) } continue } // Check for modifications in string (s1) or numeric (v1) values var newValue interface{} modified := false // Check if s1 was modified sVal := L.GetGlobal("s1") if s, ok := sVal.(lua.LString); ok && string(s) != val { newValue = string(s) modified = true } else { // Check if v1 was modified to a number vVal := L.GetGlobal("v1") if v, ok := vVal.(lua.LNumber); ok { numStr := strconv.FormatFloat(float64(v), 'f', -1, 64) if numStr != val { newValue = numStr modified = true } } } // Apply the modification if anything changed if modified { err := p.setValueAtPath(modifiedDoc, path, newValue) if err != nil { if p.Logger != nil { p.Logger.Printf("Error updating value at path %v: %v", path, err) } continue } modificationCount++ modifications = append(modifications, ModificationRecord{ File: "", OldValue: val, NewValue: fmt.Sprintf("%v", newValue), Operation: originalExpr, Context: fmt.Sprintf("(JSONPath: %s)", pattern), }) if p.Logger != nil { p.Logger.Printf("Modified string node #%d: '%s' -> '%s'", i+1, LimitString(val, 30), LimitString(fmt.Sprintf("%v", newValue), 30)) } } } } // Marshal the modified document back to JSON with indentation if modificationCount > 0 { modifiedJSON, err := json.MarshalIndent(modifiedDoc, "", " ") if err != nil { return "", 0, 0, fmt.Errorf("error marshaling modified JSON: %v", err) } if p.Logger != nil { p.Logger.Printf("Made %d JSON node modifications", modificationCount) } return string(modifiedJSON), modificationCount, matchCount, nil } // If no modifications were made, return the original content return content, 0, matchCount, nil } // findNodePaths implements a simplified JSONPath for finding paths to nodes func (p *JSONProcessor) findNodePaths(doc interface{}, path string) ([][]interface{}, error) { // Validate the path has proper syntax if strings.Contains(path, "[[") || strings.Contains(path, "]]") { return nil, fmt.Errorf("invalid JSONPath syntax: %s", path) } // Handle root element special case if path == "$" { return [][]interface{}{{doc}}, nil } // Split path into segments segments := strings.Split(strings.TrimPrefix(path, "$."), ".") // Start with the root current := [][]interface{}{{doc}} // Process each segment for _, segment := range segments { var next [][]interface{} // Handle array notation [*] if segment == "[*]" || strings.HasSuffix(segment, "[*]") { baseName := strings.TrimSuffix(segment, "[*]") for _, path := range current { item := path[len(path)-1] // Get the last item in the path switch v := item.(type) { case map[string]interface{}: if baseName == "" { // [*] means all elements at this level for _, val := range v { if arr, ok := val.([]interface{}); ok { for i, elem := range arr { newPath := make([]interface{}, len(path)+2) copy(newPath, path) newPath[len(path)] = i // Array index newPath[len(path)+1] = elem next = append(next, newPath) } } } } else if arr, ok := v[baseName].([]interface{}); ok { for i, elem := range arr { newPath := make([]interface{}, len(path)+3) copy(newPath, path) newPath[len(path)] = baseName newPath[len(path)+1] = i // Array index newPath[len(path)+2] = elem next = append(next, newPath) } } case []interface{}: for i, elem := range v { newPath := make([]interface{}, len(path)+1) copy(newPath, path) newPath[len(path)-1] = i // Replace last elem with index newPath[len(path)] = elem next = append(next, newPath) } } } current = next continue } // Handle specific array indices if strings.Contains(segment, "[") && strings.Contains(segment, "]") { // Validate proper array syntax if !regexp.MustCompile(`\[\d+\]$`).MatchString(segment) { return nil, fmt.Errorf("invalid array index in JSONPath: %s", segment) } // Extract base name and index baseName := segment[:strings.Index(segment, "[")] idxStr := segment[strings.Index(segment, "[")+1 : strings.Index(segment, "]")] idx, err := strconv.Atoi(idxStr) if err != nil { return nil, fmt.Errorf("invalid array index: %s", idxStr) } for _, path := range current { item := path[len(path)-1] // Get the last item in the path if obj, ok := item.(map[string]interface{}); ok { if arr, ok := obj[baseName].([]interface{}); ok && idx < len(arr) { newPath := make([]interface{}, len(path)+3) copy(newPath, path) newPath[len(path)] = baseName newPath[len(path)+1] = idx newPath[len(path)+2] = arr[idx] next = append(next, newPath) } } } current = next continue } // Handle regular object properties for _, path := range current { item := path[len(path)-1] // Get the last item in the path if obj, ok := item.(map[string]interface{}); ok { if val, exists := obj[segment]; exists { newPath := make([]interface{}, len(path)+2) copy(newPath, path) newPath[len(path)] = segment newPath[len(path)+1] = val next = append(next, newPath) } } } current = next } return current, nil } // getValueAtPath extracts a value from a JSON document at the specified path func (p *JSONProcessor) getValueAtPath(doc interface{}, path []interface{}) (interface{}, error) { if len(path) == 0 { return nil, fmt.Errorf("empty path") } // The last element in the path is the value itself return path[len(path)-1], nil } // setValueAtPath updates a value in a JSON document at the specified path func (p *JSONProcessor) setValueAtPath(doc interface{}, path []interface{}, newValue interface{}) error { if len(path) < 2 { return fmt.Errorf("path too short to update value") } // The path structure alternates: object/key/object/key/.../finalObject/finalKey/value // We need to navigate to the object containing our key // We'll get the parent object and the key to modify // Find the parent object (second to last object) and the key (last object's property name) // For the path structure, the parent is at index len-3 and key at len-2 if len(path) < 3 { // Simple case: directly update the root object rootObj, ok := doc.(map[string]interface{}) if !ok { return fmt.Errorf("root is not an object, cannot update") } // Key should be a string key, ok := path[len(path)-2].(string) if !ok { return fmt.Errorf("key is not a string: %v", path[len(path)-2]) } rootObj[key] = newValue return nil } // More complex case: we need to navigate to the parent object parentIdx := len(path) - 3 keyIdx := len(path) - 2 // The actual key we need to modify key, isString := path[keyIdx].(string) keyInt, isInt := path[keyIdx].(int) if !isString && !isInt { return fmt.Errorf("key must be string or int, got %T", path[keyIdx]) } // Get the parent object that contains the key parent := path[parentIdx] // If parent is a map, use string key if parentMap, ok := parent.(map[string]interface{}); ok && isString { parentMap[key] = newValue return nil } // If parent is an array, use int key if parentArray, ok := parent.([]interface{}); ok && isInt { if keyInt < 0 || keyInt >= len(parentArray) { return fmt.Errorf("array index %d out of bounds [0,%d)", keyInt, len(parentArray)) } parentArray[keyInt] = newValue return nil } return fmt.Errorf("cannot update value: parent is %T and key is %T", parent, path[keyIdx]) } // SetupJSONHelpers adds JSON-specific helper functions to Lua func (p *JSONProcessor) SetupJSONHelpers(L *lua.LState) { // Helper to get type of JSON value L.SetGlobal("json_type", L.NewFunction(func(L *lua.LState) int { // Get the value passed to the function val := L.Get(1) // Determine type switch val.Type() { case lua.LTNil: L.Push(lua.LString("null")) case lua.LTBool: L.Push(lua.LString("boolean")) case lua.LTNumber: L.Push(lua.LString("number")) case lua.LTString: L.Push(lua.LString("string")) case lua.LTTable: // Could be object or array - check for numeric keys isArray := true table := val.(*lua.LTable) table.ForEach(func(key, value lua.LValue) { if key.Type() != lua.LTNumber { isArray = false } }) if isArray { L.Push(lua.LString("array")) } else { L.Push(lua.LString("object")) } default: L.Push(lua.LString("unknown")) } return 1 })) } // jsonToLua converts a Go JSON value to a Lua value func (p *JSONProcessor) jsonToLua(L *lua.LState, val interface{}) lua.LValue { if val == nil { return lua.LNil } switch v := val.(type) { case bool: return lua.LBool(v) case float64: return lua.LNumber(v) case string: return lua.LString(v) case []interface{}: arr := L.NewTable() for i, item := range v { arr.RawSetInt(i+1, p.jsonToLua(L, item)) } return arr case map[string]interface{}: obj := L.NewTable() for k, item := range v { obj.RawSetString(k, p.jsonToLua(L, item)) } return obj default: // For unknown types, convert to string representation return lua.LString(fmt.Sprintf("%v", val)) } } // luaToJSON converts a Lua value to a Go JSON-compatible value func (p *JSONProcessor) luaToJSON(val lua.LValue) interface{} { switch val.Type() { case lua.LTNil: return nil case lua.LTBool: return lua.LVAsBool(val) case lua.LTNumber: return float64(val.(lua.LNumber)) case lua.LTString: return val.String() case lua.LTTable: table := val.(*lua.LTable) // Check if it's an array or an object isArray := true maxN := 0 table.ForEach(func(key, _ lua.LValue) { if key.Type() == lua.LTNumber { n := int(key.(lua.LNumber)) if n > maxN { maxN = n } } else { isArray = false } }) if isArray && maxN > 0 { // It's an array arr := make([]interface{}, maxN) for i := 1; i <= maxN; i++ { item := table.RawGetInt(i) if item != lua.LNil { arr[i-1] = p.luaToJSON(item) } } return arr } else { // It's an object obj := make(map[string]interface{}) table.ForEach(func(key, value lua.LValue) { if key.Type() == lua.LTString { obj[key.String()] = p.luaToJSON(value) } else { // Convert key to string if it's not already obj[fmt.Sprintf("%v", key)] = p.luaToJSON(value) } }) return obj } default: // For functions, userdata, etc., convert to string return val.String() } }