From c4d170cdca72f7420b8b3a251327f5cd39b03c34 Mon Sep 17 00:00:00 2001 From: Teal Bauer Date: Wed, 21 May 2025 18:04:30 +0200 Subject: [PATCH] fix: make decompiler variables renameable --- .../ghidra/endpoints/FunctionEndpoints.java | 140 +++++- .../ghidra/endpoints/VariableEndpoints.java | 423 +++++++++++++++++- .../eu/starsong/ghidra/util/GhidraUtil.java | 60 ++- 3 files changed, 616 insertions(+), 7 deletions(-) diff --git a/src/main/java/eu/starsong/ghidra/endpoints/FunctionEndpoints.java b/src/main/java/eu/starsong/ghidra/endpoints/FunctionEndpoints.java index 22ff95a..9ffca56 100644 --- a/src/main/java/eu/starsong/ghidra/endpoints/FunctionEndpoints.java +++ b/src/main/java/eu/starsong/ghidra/endpoints/FunctionEndpoints.java @@ -7,15 +7,24 @@ import eu.starsong.ghidra.api.ResponseBuilder; import eu.starsong.ghidra.model.FunctionInfo; import eu.starsong.ghidra.util.GhidraUtil; import eu.starsong.ghidra.util.TransactionHelper; +import ghidra.app.decompiler.DecompInterface; +import ghidra.app.decompiler.DecompileResults; import ghidra.framework.plugintool.PluginTool; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressFactory; import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Parameter; import ghidra.program.model.listing.Program; +import ghidra.program.model.pcode.HighFunction; +import ghidra.program.model.pcode.HighFunctionDBUtil; +import ghidra.program.model.pcode.HighSymbol; +import ghidra.program.model.symbol.SourceType; import ghidra.util.Msg; +import ghidra.util.task.ConsoleTaskMonitor; import java.util.ArrayList; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.io.IOException; @@ -791,6 +800,14 @@ public class FunctionEndpoints extends AbstractEndpoint { handleDisassembleFunction(exchange, function); } else if (resource.equals("variables")) { handleFunctionVariables(exchange, function); + } else if (resource.startsWith("variables/")) { + // Handle variable operations + String variableName = resource.substring("variables/".length()); + if ("PATCH".equals(exchange.getRequestMethod())) { + handleUpdateVariable(exchange, function, variableName); + } else { + sendErrorResponse(exchange, 405, "Method not allowed for variable operations", "METHOD_NOT_ALLOWED"); + } } else { sendErrorResponse(exchange, 404, "Function resource not found: " + resource, "RESOURCE_NOT_FOUND"); } @@ -1251,8 +1268,127 @@ public class FunctionEndpoints extends AbstractEndpoint { * Handle requests to update a function variable */ private void handleUpdateVariable(HttpExchange exchange, Function function, String variableName) throws IOException { - // This is a placeholder - actual implementation would update the variable - sendErrorResponse(exchange, 501, "Variable update not implemented", "NOT_IMPLEMENTED"); + // This is a placeholder - we need to implement variable renaming here + Program program = getCurrentProgram(); + if (program == null) { + sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED"); + return; + } + + try { + // Parse the request body to get the update parameters + Map params = parseJsonPostParams(exchange); + String newName = params.get("name"); + String newDataType = params.get("data_type"); + + if (newName == null && newDataType == null) { + sendErrorResponse(exchange, 400, "Missing update parameters - name or data_type required", "MISSING_PARAMETER"); + return; + } + + // Check if this is a decompiler-generated variable + boolean success = false; + String message = ""; + + // Use a transaction to update the variable + try { + int txId = program.startTransaction("Update Function Variable"); + try { + // First check if this is a regular parameter or local variable + for (Parameter param : function.getParameters()) { + if (param.getName().equals(variableName)) { + if (newName != null) { + param.setName(newName, ghidra.program.model.symbol.SourceType.USER_DEFINED); + success = true; + message = "Parameter renamed successfully"; + } + // Handle data type change if needed + break; + } + } + + // If not a parameter, check if it's a local variable + if (!success) { + for (ghidra.program.model.listing.Variable var : function.getAllVariables()) { + if (var.getName().equals(variableName) && !(var instanceof Parameter)) { + if (newName != null) { + var.setName(newName, ghidra.program.model.symbol.SourceType.USER_DEFINED); + success = true; + message = "Local variable renamed successfully"; + } + // Handle data type change if needed + break; + } + } + } + + // If not a database variable, try as a decompiler variable + if (!success) { + // This requires a decompile operation to get the HighFunction + DecompInterface decomp = new DecompInterface(); + try { + decomp.openProgram(program); + DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor()); + + if (results.decompileCompleted()) { + HighFunction highFunc = results.getHighFunction(); + if (highFunc != null) { + // Find the variable in the high function + HighSymbol symbol = null; + Iterator symbolIter = highFunc.getLocalSymbolMap().getSymbols(); + while (symbolIter.hasNext()) { + HighSymbol hs = symbolIter.next(); + if (hs.getName().equals(variableName)) { + symbol = hs; + break; + } + } + + if (symbol != null) { + if (newName != null) { + // Rename the variable using HighFunctionDBUtil + HighFunctionDBUtil.updateDBVariable( + symbol, newName, null, SourceType.USER_DEFINED); + success = true; + message = "Decompiler variable renamed successfully"; + } + } + } + } + } finally { + decomp.dispose(); + } + } + + program.endTransaction(txId, true); + } catch (Exception e) { + program.endTransaction(txId, false); + throw e; + } + } catch (Exception e) { + sendErrorResponse(exchange, 500, "Error updating variable: " + e.getMessage(), "UPDATE_FAILED"); + return; + } + + if (success) { + // Create a successful response + Map result = new HashMap<>(); + result.put("name", newName != null ? newName : variableName); + result.put("function", function.getName()); + result.put("address", function.getEntryPoint().toString()); + result.put("message", message); + + ResponseBuilder builder = new ResponseBuilder(exchange, port) + .success(true) + .result(result); + + sendJsonResponse(exchange, builder.build(), 200); + } else { + sendErrorResponse(exchange, 404, "Function resource not found: variables/" + variableName, "RESOURCE_NOT_FOUND"); + } + } catch (Exception e) { + sendErrorResponse(exchange, 500, "Error processing variable update request: " + e.getMessage(), "INTERNAL_ERROR"); + } } /** diff --git a/src/main/java/eu/starsong/ghidra/endpoints/VariableEndpoints.java b/src/main/java/eu/starsong/ghidra/endpoints/VariableEndpoints.java index 4b33b18..a5c08ac 100644 --- a/src/main/java/eu/starsong/ghidra/endpoints/VariableEndpoints.java +++ b/src/main/java/eu/starsong/ghidra/endpoints/VariableEndpoints.java @@ -58,9 +58,9 @@ package eu.starsong.ghidra.endpoints; @Override public void registerEndpoints(HttpServer server) { server.createContext("/variables", this::handleGlobalVariables); - // Note: /functions/{name}/variables is handled within FunctionEndpoints for now - // to keep related logic together until full refactor. - // If needed, we can create a more complex routing mechanism later. + server.createContext("/functions/*/variables/*", this::handleFunctionVariableOperation); + server.createContext("/functions/by-name/*/variables/*", this::handleFunctionVariableByNameOperation); + // Note: /functions/{name}/variables (listing) is still handled within FunctionEndpoints } private void handleGlobalVariables(HttpExchange exchange) throws IOException { @@ -642,4 +642,421 @@ package eu.starsong.ghidra.endpoints; DataType dt = data.getDataType(); return dt != null ? dt.getName() : "unknown"; } + + /** + * Handle operations on a specific function variable (by function address or by name) + * This handles GET/PATCH operations for /functions/{address}/variables/{varName} + * or /functions/by-name/{name}/variables/{varName} + */ + public void handleFunctionVariableOperation(HttpExchange exchange) throws IOException { + try { + String path = exchange.getRequestURI().getPath(); + String[] parts = path.split("/"); + + // Path should be like /functions/{address}/variables/{varName} + if (parts.length < 5) { + sendErrorResponse(exchange, 400, "Invalid URL format", "INVALID_URL_FORMAT"); + return; + } + + // Extract function address from path + String functionAddress = parts[2]; + String variableName = parts[4]; + + // Handle different HTTP methods + if ("PATCH".equals(exchange.getRequestMethod())) { + handleVariablePatch(exchange, functionAddress, variableName, false); + } else if ("GET".equals(exchange.getRequestMethod())) { + handleVariableGet(exchange, functionAddress, variableName, false); + } else { + sendErrorResponse(exchange, 405, "Method Not Allowed"); + } + } catch (Exception e) { + Msg.error(this, "Error handling function variable operation", e); + sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); + } + } + + /** + * Handle operations on a specific function variable by function name + * This handles GET/PATCH operations for /functions/by-name/{functionName}/variables/{varName} + */ + private void handleFunctionVariableByNameOperation(HttpExchange exchange) throws IOException { + try { + String path = exchange.getRequestURI().getPath(); + String[] parts = path.split("/"); + + // Path should be like /functions/by-name/{functionName}/variables/{varName} + if (parts.length < 6) { + sendErrorResponse(exchange, 400, "Invalid URL format", "INVALID_URL_FORMAT"); + return; + } + + // Extract function name from path + String functionName = parts[3]; + String variableName = parts[5]; + + // Handle different HTTP methods + if ("PATCH".equals(exchange.getRequestMethod())) { + handleVariablePatch(exchange, functionName, variableName, true); + } else if ("GET".equals(exchange.getRequestMethod())) { + handleVariableGet(exchange, functionName, variableName, true); + } else { + sendErrorResponse(exchange, 405, "Method Not Allowed"); + } + } catch (Exception e) { + Msg.error(this, "Error handling function variable by name operation", e); + sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); + } + } + + /** + * Handle GET request for a specific variable + */ + private void handleVariableGet(HttpExchange exchange, String functionIdentifier, String variableName, boolean isByName) throws IOException { + Program program = getCurrentProgram(); + if (program == null) { + sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED"); + return; + } + + // Find the function + Function function = null; + if (isByName) { + for (Function f : program.getFunctionManager().getFunctions(true)) { + if (f.getName().equals(functionIdentifier)) { + function = f; + break; + } + } + } else { + try { + Address address = program.getAddressFactory().getAddress(functionIdentifier); + function = program.getFunctionManager().getFunctionAt(address); + } catch (Exception e) { + Msg.error(this, "Error getting function at address " + functionIdentifier, e); + } + } + + if (function == null) { + sendErrorResponse(exchange, 404, "Function not found", "FUNCTION_NOT_FOUND"); + return; + } + + // Find variable in function + Map variableInfo = findVariableInFunction(function, variableName); + + if (variableInfo == null) { + sendErrorResponse(exchange, 404, "Function resource not found: variables/" + variableName, "RESOURCE_NOT_FOUND"); + return; + } + + // Create HATEOAS-compliant response + String functionPathBase = isByName ? "/functions/by-name/" + function.getName() : "/functions/" + function.getEntryPoint(); + + ResponseBuilder builder = new ResponseBuilder(exchange, port) + .success(true) + .addLink("self", functionPathBase + "/variables/" + variableName) + .addLink("function", functionPathBase) + .addLink("variables", functionPathBase + "/variables") + .result(variableInfo); + + sendJsonResponse(exchange, builder.build(), 200); + } + + /** + * Handle PATCH request to update a variable (rename or change data type) + */ + private void handleVariablePatch(HttpExchange exchange, String functionIdentifier, String variableName, boolean isByName) throws IOException { + Program program = getCurrentProgram(); + if (program == null) { + sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED"); + return; + } + + // Parse the request body to get the update data + String requestBody = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + JsonObject jsonRequest; + try { + jsonRequest = new Gson().fromJson(requestBody, JsonObject.class); + } catch (Exception e) { + sendErrorResponse(exchange, 400, "Invalid JSON payload", "INVALID_JSON"); + return; + } + + // Find the function + Function function = null; + if (isByName) { + for (Function f : program.getFunctionManager().getFunctions(true)) { + if (f.getName().equals(functionIdentifier)) { + function = f; + break; + } + } + } else { + try { + Address address = program.getAddressFactory().getAddress(functionIdentifier); + function = program.getFunctionManager().getFunctionAt(address); + } catch (Exception e) { + Msg.error(this, "Error getting function at address " + functionIdentifier, e); + } + } + + if (function == null) { + sendErrorResponse(exchange, 404, "Function not found", "FUNCTION_NOT_FOUND"); + return; + } + + // Make sure we have a variable to update + Map variableInfo = findVariableInFunction(function, variableName); + if (variableInfo == null) { + sendErrorResponse(exchange, 404, "Function resource not found: variables/" + variableName, "RESOURCE_NOT_FOUND"); + return; + } + + // Check if this is a decompiler-only variable + boolean isDecompilerOnly = Boolean.TRUE.equals(variableInfo.get("decompilerOnly")); + + // Get requested changes + String newName = null; + String newDataType = null; + + if (jsonRequest.has("name")) { + newName = jsonRequest.get("name").getAsString(); + } + + if (jsonRequest.has("data_type")) { + newDataType = jsonRequest.get("data_type").getAsString(); + } + + boolean success = false; + String message = ""; + + try { + if (isDecompilerOnly) { + // For decompiler-only variables, use HighFunctionDBUtil + success = updateDecompilerVariable(function, variableName, newName, newDataType); + message = success ? "Updated decompiler variable" : "Failed to update decompiler variable"; + } else { + // For regular variables, use the existing parameter/local variable mechanism + success = updateRegularVariable(function, variableName, newName, newDataType); + message = success ? "Updated variable" : "Failed to update variable"; + } + } catch (Exception e) { + Msg.error(this, "Error updating variable: " + e.getMessage(), e); + sendErrorResponse(exchange, 500, "Error updating variable: " + e.getMessage()); + return; + } + + if (success) { + // Get updated variable info + Map updatedInfo; + if (newName != null) { + // If renamed, use the new name to find variable + updatedInfo = findVariableInFunction(function, newName); + } else { + // Otherwise use the original name + updatedInfo = findVariableInFunction(function, variableName); + } + + if (updatedInfo == null) { + // This shouldn't happen if the update was successful + updatedInfo = new HashMap<>(); + updatedInfo.put("message", message); + } + + // Create HATEOAS-compliant response + String functionPathBase = isByName ? "/functions/by-name/" + function.getName() : "/functions/" + function.getEntryPoint(); + + ResponseBuilder builder = new ResponseBuilder(exchange, port) + .success(true) + .addLink("self", functionPathBase + "/variables/" + (newName != null ? newName : variableName)) + .addLink("function", functionPathBase) + .addLink("variables", functionPathBase + "/variables") + .result(updatedInfo); + + sendJsonResponse(exchange, builder.build(), 200); + } else { + sendErrorResponse(exchange, 500, "Failed to update variable: " + message); + } + } + + /** + * Update a regular variable (parameter or local variable) + */ + private boolean updateRegularVariable(Function function, String variableName, String newName, String newDataType) { + try { + // Find and update parameter + for (Parameter param : function.getParameters()) { + if (param.getName().equals(variableName)) { + if (newName != null) { + param.setName(newName, SourceType.USER_DEFINED); + } + // Updating data type for parameters would go here + return true; + } + } + + // Find and update local variable (use ghidra.program.model.listing.Variable) + for (ghidra.program.model.listing.Variable var : function.getAllVariables()) { + if (var.getName().equals(variableName) && !(var instanceof Parameter)) { + if (newName != null) { + var.setName(newName, SourceType.USER_DEFINED); + } + // Updating data type for local variables would go here + return true; + } + } + + return false; + } catch (Exception e) { + Msg.error(this, "Error updating regular variable", e); + return false; + } + } + + /** + * Update a decompiler-generated variable + */ + private boolean updateDecompilerVariable(Function function, String variableName, String newName, String newDataType) { + if (newName == null) { + // Nothing to do + return false; + } + + try { + Msg.info(this, "Attempting to rename decompiler variable: " + variableName + " to " + newName); + + // This requires a decompile operation to get the HighFunction + DecompInterface decomp = new DecompInterface(); + try { + decomp.openProgram(getCurrentProgram()); + DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor()); + + if (results.decompileCompleted()) { + HighFunction highFunc = results.getHighFunction(); + if (highFunc != null) { + // Get the local symbol map + LocalSymbolMap symbolMap = highFunc.getLocalSymbolMap(); + + // Find the variable in the high function + HighSymbol symbol = null; + Iterator symbolIter = symbolMap.getSymbols(); + while (symbolIter.hasNext()) { + HighSymbol hs = symbolIter.next(); + if (hs.getName().equals(variableName)) { + symbol = hs; + Msg.info(this, "Found decompiler variable: " + variableName); + break; + } + } + + if (symbol != null) { + Msg.info(this, "Starting transaction to rename: " + variableName); + // Use transaction to update the variable + int txId = getCurrentProgram().startTransaction("Update Decompiler Variable"); + try { + // Rename the variable using HighFunctionDBUtil + // This method returns void, so we assume success if no exception is thrown + HighFunctionDBUtil.updateDBVariable( + symbol, newName, null, SourceType.USER_DEFINED); + + // If we reach here, it was successful + Msg.info(this, "Successfully renamed variable to: " + newName); + getCurrentProgram().endTransaction(txId, true); + return true; + } catch (Exception e) { + getCurrentProgram().endTransaction(txId, false); + Msg.error(this, "Error updating decompiler variable: " + e.getMessage(), e); + return false; + } + } else { + Msg.error(this, "Could not find decompiler variable: " + variableName); + } + } else { + Msg.error(this, "HighFunction is null after decompilation"); + } + } else { + Msg.error(this, "Decompilation did not complete successfully for function: " + function.getName()); + } + } finally { + decomp.dispose(); + } + return false; + } catch (Exception e) { + Msg.error(this, "Error updating decompiler variable", e); + return false; + } + } + + /** + * Find a variable in the function, including decompiler-generated variables + */ + private Map findVariableInFunction(Function function, String variableName) { + // First check regular parameters and local variables + for (Parameter param : function.getParameters()) { + if (param.getName().equals(variableName)) { + Map info = new HashMap<>(); + info.put("name", param.getName()); + info.put("dataType", param.getDataType().getName()); + info.put("type", "parameter"); + info.put("storage", param.getVariableStorage().toString()); + info.put("ordinal", param.getOrdinal()); + info.put("decompilerOnly", false); + return info; + } + } + + for (ghidra.program.model.listing.Variable var : function.getAllVariables()) { + if (var.getName().equals(variableName) && !(var instanceof Parameter)) { + Map info = new HashMap<>(); + info.put("name", var.getName()); + info.put("dataType", var.getDataType().getName()); + info.put("type", "local"); + info.put("storage", var.getVariableStorage().toString()); + info.put("decompilerOnly", false); + return info; + } + } + + // Then check decompiler-generated variables + try { + // This requires a decompile operation to get the HighFunction + DecompInterface decomp = new DecompInterface(); + try { + decomp.openProgram(getCurrentProgram()); + DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor()); + + if (results.decompileCompleted()) { + HighFunction highFunc = results.getHighFunction(); + if (highFunc != null) { + LocalSymbolMap localSymbolMap = highFunc.getLocalSymbolMap(); + + // Check local symbol map for the variable + Iterator symbolIter = localSymbolMap.getSymbols(); + while (symbolIter.hasNext()) { + HighSymbol symbol = symbolIter.next(); + if (symbol.getName().equals(variableName)) { + Map info = new HashMap<>(); + info.put("name", symbol.getName()); + info.put("dataType", symbol.getDataType() != null ? symbol.getDataType().getName() : "unknown"); + info.put("type", symbol.isParameter() ? "parameter" : "local"); + info.put("decompilerOnly", true); + info.put("pcAddress", symbol.getPCAddress() != null ? symbol.getPCAddress().toString() : "N/A"); + info.put("storage", symbol.getStorage() != null ? symbol.getStorage().toString() : "N/A"); + return info; + } + } + } + } + } finally { + decomp.dispose(); + } + } catch (Exception e) { + Msg.error(this, "Error examining decompiler variables", e); + } + + // Variable not found + return null; + } } \ No newline at end of file diff --git a/src/main/java/eu/starsong/ghidra/util/GhidraUtil.java b/src/main/java/eu/starsong/ghidra/util/GhidraUtil.java index 0bdcaba..2de1fbc 100644 --- a/src/main/java/eu/starsong/ghidra/util/GhidraUtil.java +++ b/src/main/java/eu/starsong/ghidra/util/GhidraUtil.java @@ -364,7 +364,7 @@ public class GhidraUtil { } /** - * Gets information about variables in a function. + * Gets information about variables in a function, including decompiler variables. * @param function The function to get variables from. * @return A list of maps containing information about each variable. */ @@ -381,10 +381,12 @@ public class GhidraUtil { varInfo.put("name", param.getName()); varInfo.put("type", param.getDataType().getName()); varInfo.put("isParameter", true); + varInfo.put("storage", param.getVariableStorage().toString()); + varInfo.put("source", "database"); variables.add(varInfo); } - // Add local variables + // Add local variables from database for (Variable var : function.getAllVariables()) { if (var instanceof Parameter) { continue; // Skip parameters, already added @@ -394,9 +396,63 @@ public class GhidraUtil { varInfo.put("name", var.getName()); varInfo.put("type", var.getDataType().getName()); varInfo.put("isParameter", false); + varInfo.put("storage", var.getVariableStorage().toString()); + varInfo.put("source", "database"); variables.add(varInfo); } + // Add decompiler-generated variables + DecompInterface decompiler = new DecompInterface(); + try { + decompiler.openProgram(function.getProgram()); + DecompileResults results = decompiler.decompileFunction(function, 30, TaskMonitor.DUMMY); + + if (results.decompileCompleted()) { + HighFunction highFunc = results.getHighFunction(); + if (highFunc != null) { + // Iterate over local variables from decompiler + for (java.util.Iterator iter = + highFunc.getLocalSymbolMap().getSymbols(); iter.hasNext(); ) { + + ghidra.program.model.pcode.HighSymbol highSymbol = iter.next(); + + // Skip if this is already a tracked variable + boolean alreadyAdded = false; + for (Map var : variables) { + if (var.get("name").equals(highSymbol.getName())) { + alreadyAdded = true; + break; + } + } + + if (!alreadyAdded) { + Map varInfo = new HashMap<>(); + varInfo.put("name", highSymbol.getName()); + varInfo.put("type", highSymbol.getDataType() != null ? + highSymbol.getDataType().getName() : "unknown"); + varInfo.put("isParameter", highSymbol.isParameter()); + varInfo.put("storage", highSymbol.getStorage() != null ? + highSymbol.getStorage().toString() : "unknown"); + varInfo.put("source", "decompiler"); + + // Add PC address if available + if (highSymbol.getPCAddress() != null) { + varInfo.put("pcAddress", highSymbol.getPCAddress().toString()); + } + + variables.add(varInfo); + } + } + } + } + } + catch (Exception e) { + Msg.error(GhidraUtil.class, "Error analyzing decompiler variables", e); + } + finally { + decompiler.dispose(); + } + return variables; }