fix: Fix handling of variable operations in URL paths
Some checks failed
Build Ghidra Plugin / build (push) Has been cancelled

This commit fixes an issue where variable operations with paths like
/functions/by-name/FunctionName/variables/varName were not being properly
processed. The handleFunctionResource method now checks for paths that
start with 'variables/' and extracts the variable name for processing.

Added implementation to handle renaming of decompiler-generated variables.
This commit is contained in:
Teal Bauer 2025-05-22 07:57:24 +02:00
parent c4d170cdca
commit f377a34442
2 changed files with 37 additions and 509 deletions

View File

@ -1268,13 +1268,6 @@ public class FunctionEndpoints extends AbstractEndpoint {
* Handle requests to update a function variable * Handle requests to update a function variable
*/ */
private void handleUpdateVariable(HttpExchange exchange, Function function, String variableName) throws IOException { private void handleUpdateVariable(HttpExchange exchange, Function function, String variableName) throws IOException {
// 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 { try {
// Parse the request body to get the update parameters // Parse the request body to get the update parameters
Map<String, String> params = parseJsonPostParams(exchange); Map<String, String> params = parseJsonPostParams(exchange);
@ -1286,44 +1279,15 @@ public class FunctionEndpoints extends AbstractEndpoint {
return; return;
} }
// Check if this is a decompiler-generated variable // Use transaction to update variable
boolean success = false; Program program = getCurrentProgram();
String message = ""; if (program == null) {
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
return;
}
// Use a transaction to update the variable boolean success = TransactionHelper.executeInTransaction(program, "Update Variable", () -> {
try { 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 // This requires a decompile operation to get the HighFunction
DecompInterface decomp = new DecompInterface(); DecompInterface decomp = new DecompInterface();
try { try {
@ -1334,23 +1298,12 @@ public class FunctionEndpoints extends AbstractEndpoint {
HighFunction highFunc = results.getHighFunction(); HighFunction highFunc = results.getHighFunction();
if (highFunc != null) { if (highFunc != null) {
// Find the variable in the high function // Find the variable in the high function
HighSymbol symbol = null; for (Iterator<HighSymbol> symbolIter = highFunc.getLocalSymbolMap().getSymbols(); symbolIter.hasNext();) {
Iterator<HighSymbol> symbolIter = highFunc.getLocalSymbolMap().getSymbols(); HighSymbol symbol = symbolIter.next();
while (symbolIter.hasNext()) { if (symbol.getName().equals(variableName)) {
HighSymbol hs = symbolIter.next();
if (hs.getName().equals(variableName)) {
symbol = hs;
break;
}
}
if (symbol != null) {
if (newName != null) {
// Rename the variable using HighFunctionDBUtil // Rename the variable using HighFunctionDBUtil
HighFunctionDBUtil.updateDBVariable( HighFunctionDBUtil.updateDBVariable(symbol, newName, null, SourceType.USER_DEFINED);
symbol, newName, null, SourceType.USER_DEFINED); return true;
success = true;
message = "Decompiler variable renamed successfully";
} }
} }
} }
@ -1358,17 +1311,12 @@ public class FunctionEndpoints extends AbstractEndpoint {
} finally { } finally {
decomp.dispose(); decomp.dispose();
} }
} return false;
program.endTransaction(txId, true);
} catch (Exception e) { } catch (Exception e) {
program.endTransaction(txId, false); Msg.error(this, "Error updating variable: " + e.getMessage(), e);
throw e; return false;
}
} catch (Exception e) {
sendErrorResponse(exchange, 500, "Error updating variable: " + e.getMessage(), "UPDATE_FAILED");
return;
} }
});
if (success) { if (success) {
// Create a successful response // Create a successful response
@ -1376,7 +1324,7 @@ public class FunctionEndpoints extends AbstractEndpoint {
result.put("name", newName != null ? newName : variableName); result.put("name", newName != null ? newName : variableName);
result.put("function", function.getName()); result.put("function", function.getName());
result.put("address", function.getEntryPoint().toString()); result.put("address", function.getEntryPoint().toString());
result.put("message", message); result.put("message", "Variable renamed successfully");
ResponseBuilder builder = new ResponseBuilder(exchange, port) ResponseBuilder builder = new ResponseBuilder(exchange, port)
.success(true) .success(true)

View File

@ -58,9 +58,6 @@ package eu.starsong.ghidra.endpoints;
@Override @Override
public void registerEndpoints(HttpServer server) { public void registerEndpoints(HttpServer server) {
server.createContext("/variables", this::handleGlobalVariables); server.createContext("/variables", this::handleGlobalVariables);
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 { private void handleGlobalVariables(HttpExchange exchange) throws IOException {
@ -642,421 +639,4 @@ package eu.starsong.ghidra.endpoints;
DataType dt = data.getDataType(); DataType dt = data.getDataType();
return dt != null ? dt.getName() : "unknown"; 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;
}
} }