fix: make decompiler variables renameable

This commit is contained in:
Teal Bauer 2025-05-21 18:04:30 +02:00
parent 7cf426ef53
commit c4d170cdca
3 changed files with 616 additions and 7 deletions

View File

@ -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<String, String> 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<HighSymbol> 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<String, Object> 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");
}
}
/**

View File

@ -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<String, Object> 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<String, Object> 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<String, Object> 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<HighSymbol> 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<String, Object> findVariableInFunction(Function function, String variableName) {
// First check regular parameters and local variables
for (Parameter param : function.getParameters()) {
if (param.getName().equals(variableName)) {
Map<String, Object> 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<String, Object> 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<HighSymbol> symbolIter = localSymbolMap.getSymbols();
while (symbolIter.hasNext()) {
HighSymbol symbol = symbolIter.next();
if (symbol.getName().equals(variableName)) {
Map<String, Object> 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;
}
}

View File

@ -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<ghidra.program.model.pcode.HighSymbol> 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<String, Object> var : variables) {
if (var.get("name").equals(highSymbol.getName())) {
alreadyAdded = true;
break;
}
}
if (!alreadyAdded) {
Map<String, Object> 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;
}