mcghidra/src/main/java/eu/starsong/ghidra/endpoints/FunctionEndpoints.java

1423 lines
62 KiB
Java

package eu.starsong.ghidra.endpoints;
import com.google.gson.JsonObject;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
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;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
/**
* Endpoints for managing functions within a program.
* Implements the /functions endpoints with HATEOAS pattern.
*/
public class FunctionEndpoints extends AbstractEndpoint {
private PluginTool tool;
public FunctionEndpoints(Program program, int port) {
super(program, port);
}
public FunctionEndpoints(Program program, int port, PluginTool tool) {
super(program, port);
this.tool = tool;
}
@Override
protected PluginTool getTool() {
return tool;
}
@Override
public void registerEndpoints(HttpServer server) {
// Register endpoints in order from most specific to least specific to ensure proper URL path matching
// Specifically handle sub-resource endpoints first (these are the most specific)
server.createContext("/functions/by-name/", this::handleFunctionByName);
// Then handle address-based endpoints with clear pattern matching
server.createContext("/functions/", this::handleFunctionByAddress);
// Base endpoint last as it's least specific
server.createContext("/functions", this::handleFunctions);
// Register function-specific endpoints
registerAdditionalEndpoints(server);
}
/**
* Register additional convenience endpoints
*/
private void registerAdditionalEndpoints(HttpServer server) {
// NOTE: The /function endpoint is already registered in ProgramEndpoints
// We don't register it here to avoid duplicating functionality
}
/**
* Handle requests to the /functions/{address} endpoint
*/
private void handleFunctionByAddress(HttpExchange exchange) throws IOException {
try {
String path = exchange.getRequestURI().getPath();
// Check if this is the base endpoint
if (path.equals("/functions") || path.equals("/functions/")) {
handleFunctions(exchange);
return;
}
// Get the current program
Program program = getCurrentProgram();
if (program == null) {
sendErrorResponse(exchange, 503, "No program is currently loaded", "NO_PROGRAM_LOADED");
return;
}
// Extract function address from path
String functionAddress = path.substring("/functions/".length());
// Check for nested resources
if (functionAddress.contains("/")) {
String resource = functionAddress.substring(functionAddress.indexOf('/') + 1);
functionAddress = functionAddress.substring(0, functionAddress.indexOf('/'));
handleFunctionResource(exchange, functionAddress, resource);
return;
}
Function function = findFunctionByAddress(functionAddress);
if (function == null) {
sendErrorResponse(exchange, 404, "Function not found at address: " + functionAddress, "FUNCTION_NOT_FOUND");
return;
}
String method = exchange.getRequestMethod();
if ("GET".equals(method)) {
// Get function details using RESTful response structure
FunctionInfo info = buildFunctionInfo(function);
ResponseBuilder builder = new ResponseBuilder(exchange, port)
.success(true)
.result(info);
// Add HATEOAS links
String baseUrl = "/functions/" + functionAddress;
builder.addLink("self", baseUrl);
builder.addLink("program", "/program");
builder.addLink("decompile", baseUrl + "/decompile");
builder.addLink("disassembly", baseUrl + "/disassembly");
builder.addLink("variables", baseUrl + "/variables");
builder.addLink("by_name", "/functions/by-name/" + function.getName());
// Add xrefs links
builder.addLink("xrefs_to", "/xrefs?to_addr=" + function.getEntryPoint());
builder.addLink("xrefs_from", "/xrefs?from_addr=" + function.getEntryPoint());
sendJsonResponse(exchange, builder.build(), 200);
} else if ("PATCH".equals(method)) {
// Update function
handleUpdateFunctionRESTful(exchange, function);
} else if ("DELETE".equals(method)) {
// Delete function
handleDeleteFunctionRESTful(exchange, function);
} else {
sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED");
}
} catch (Exception e) {
Msg.error(this, "Error handling /functions/{address} endpoint", e);
sendErrorResponse(exchange, 500, "Internal Server Error: " + e.getMessage(), "INTERNAL_ERROR");
}
}
/**
* Handle requests to the /functions/by-name/{name} endpoint
*/
private void handleFunctionByName(HttpExchange exchange) throws IOException {
try {
String path = exchange.getRequestURI().getPath();
// Extract function name from path (only supporting new format)
String functionName = path.substring("/functions/by-name/".length());
// Check for nested resources
if (functionName.contains("/")) {
String resource = functionName.substring(functionName.indexOf('/') + 1);
functionName = functionName.substring(0, functionName.indexOf('/'));
handleFunctionResource(exchange, functionName, resource);
return;
}
Function function = findFunctionByName(functionName);
if (function == null) {
sendErrorResponse(exchange, 404, "Function not found with name: " + functionName, "FUNCTION_NOT_FOUND");
return;
}
String method = exchange.getRequestMethod();
if ("GET".equals(method)) {
// Get function details using RESTful response structure
FunctionInfo info = buildFunctionInfo(function);
ResponseBuilder builder = new ResponseBuilder(exchange, port)
.success(true)
.result(info);
// Add HATEOAS links
builder.addLink("self", "/functions/by-name/" + functionName);
builder.addLink("program", "/program");
builder.addLink("by_address", "/functions/" + function.getEntryPoint());
builder.addLink("decompile", "/functions/" + function.getEntryPoint() + "/decompile");
builder.addLink("disassembly", "/functions/" + function.getEntryPoint() + "/disassembly");
builder.addLink("variables", "/functions/by-name/" + functionName + "/variables");
sendJsonResponse(exchange, builder.build(), 200);
} else if ("PATCH".equals(method)) {
// Update function
handleUpdateFunctionRESTful(exchange, function);
} else {
sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED");
}
} catch (Exception e) {
Msg.error(this, "Error handling /programs/current/functions/by-name/{name} endpoint", e);
sendErrorResponse(exchange, 500, "Internal Server Error: " + e.getMessage(), "INTERNAL_ERROR");
}
}
/**
* Handle requests to all functions within the current program
*/
private void handleProgramFunctions(HttpExchange exchange) throws IOException {
try {
if ("GET".equals(exchange.getRequestMethod())) {
Map<String, String> params = parseQueryParams(exchange);
int offset = parseIntOrDefault(params.get("offset"), 0);
int limit = parseIntOrDefault(params.get("limit"), 100);
String nameFilter = params.get("name");
String nameContainsFilter = params.get("name_contains");
String nameRegexFilter = params.get("name_matches_regex");
String addrFilter = params.get("addr");
Program program = getCurrentProgram();
if (program == null) {
sendErrorResponse(exchange, 400, "No program is currently loaded", "NO_PROGRAM_LOADED");
return;
}
List<Map<String, Object>> functions = new ArrayList<>();
// Get all functions
for (Function f : program.getFunctionManager().getFunctions(true)) {
// Apply filters
if (nameFilter != null && !f.getName().equals(nameFilter)) {
continue;
}
if (nameContainsFilter != null && !f.getName().toLowerCase().contains(nameContainsFilter.toLowerCase())) {
continue;
}
if (nameRegexFilter != null && !f.getName().matches(nameRegexFilter)) {
continue;
}
if (addrFilter != null && !f.getEntryPoint().toString().equals(addrFilter)) {
continue;
}
Map<String, Object> func = new HashMap<>();
func.put("name", f.getName());
func.put("address", f.getEntryPoint().toString());
// Add HATEOAS links
Map<String, Object> links = new HashMap<>();
Map<String, String> selfLink = new HashMap<>();
selfLink.put("href", "/programs/current/functions/" + f.getEntryPoint());
links.put("self", selfLink);
Map<String, String> byNameLink = new HashMap<>();
byNameLink.put("href", "/programs/current/functions/by-name/" + f.getName());
links.put("by_name", byNameLink);
Map<String, String> decompileLink = new HashMap<>();
decompileLink.put("href", "/programs/current/functions/" + f.getEntryPoint() + "/decompile");
links.put("decompile", decompileLink);
func.put("_links", links);
functions.add(func);
}
// Apply pagination
int endIndex = Math.min(functions.size(), offset + limit);
List<Map<String, Object>> paginatedFunctions = offset < functions.size()
? functions.subList(offset, endIndex)
: new ArrayList<>();
// Build response with pagination links
ResponseBuilder builder = new ResponseBuilder(exchange, port)
.success(true)
.result(paginatedFunctions);
// Add pagination metadata
Map<String, Object> metadata = new HashMap<>();
metadata.put("size", functions.size());
metadata.put("offset", offset);
metadata.put("limit", limit);
builder.metadata(metadata);
// Add query parameters for self link
StringBuilder queryParams = new StringBuilder();
if (nameFilter != null) {
queryParams.append("name=").append(nameFilter).append("&");
}
if (nameContainsFilter != null) {
queryParams.append("name_contains=").append(nameContainsFilter).append("&");
}
if (nameRegexFilter != null) {
queryParams.append("name_matches_regex=").append(nameRegexFilter).append("&");
}
if (addrFilter != null) {
queryParams.append("addr=").append(addrFilter).append("&");
}
String queryString = queryParams.toString();
// Add HATEOAS links
builder.addLink("self", "/programs/current/functions?" + queryString + "offset=" + offset + "&limit=" + limit);
builder.addLink("program", "/programs/current");
// Add next/prev links if applicable
if (endIndex < functions.size()) {
builder.addLink("next", "/programs/current/functions?" + queryString + "offset=" + endIndex + "&limit=" + limit);
}
if (offset > 0) {
int prevOffset = Math.max(0, offset - limit);
builder.addLink("prev", "/programs/current/functions?" + queryString + "offset=" + prevOffset + "&limit=" + limit);
}
// Add link to create a new function
builder.addLink("create", "/programs/current/functions", "POST");
sendJsonResponse(exchange, builder.build(), 200);
} else if ("POST".equals(exchange.getRequestMethod())) {
// Create a new function
handleCreateFunctionRESTful(exchange);
} else {
sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED");
}
} catch (Exception e) {
Msg.error(this, "Error handling /programs/current/functions endpoint", e);
sendErrorResponse(exchange, 500, "Internal Server Error: " + e.getMessage(), "INTERNAL_ERROR");
}
}
/**
* Handle requests to function resources like /programs/current/functions/{address}/decompile
*/
private void handleFunctionResourceRESTful(HttpExchange exchange, String functionAddress, String resource) throws IOException {
Function function = findFunctionByAddress(functionAddress);
if (function == null) {
sendErrorResponse(exchange, 404, "Function not found at address: " + functionAddress, "FUNCTION_NOT_FOUND");
return;
}
if (resource.equals("decompile")) {
handleDecompileFunction(exchange, function);
} else if (resource.equals("disassembly")) {
handleDisassembleFunction(exchange, function);
} else if (resource.equals("variables")) {
handleFunctionVariables(exchange, function);
} else {
sendErrorResponse(exchange, 404, "Function resource not found: " + resource, "RESOURCE_NOT_FOUND");
}
}
/**
* Handle requests to function resources by name like /programs/current/functions/by-name/{name}/variables
*/
private void handleFunctionResourceByNameRESTful(HttpExchange exchange, String functionName, String resource) throws IOException {
Function function = findFunctionByName(functionName);
if (function == null) {
sendErrorResponse(exchange, 404, "Function not found with name: " + functionName, "FUNCTION_NOT_FOUND");
return;
}
if (resource.equals("variables")) {
handleFunctionVariables(exchange, function);
} else if (resource.equals("decompile")) {
handleDecompileFunction(exchange, function);
} else if (resource.equals("disassembly")) {
handleDisassembleFunction(exchange, function);
} else {
sendErrorResponse(exchange, 404, "Function resource not found: " + resource, "RESOURCE_NOT_FOUND");
}
}
/**
* Handle PATCH requests to update a function using the RESTful endpoint
*/
private void handleUpdateFunctionRESTful(HttpExchange exchange, Function function) throws IOException {
// Implementation similar to handleUpdateFunction but with RESTful response structure
Program program = getCurrentProgram();
if (program == null) {
sendErrorResponse(exchange, 400, "No program is currently loaded", "NO_PROGRAM_LOADED");
return;
}
// Parse request body
Map<String, String> params = parseJsonPostParams(exchange);
String newName = params.get("name");
String signature = params.get("signature");
String comment = params.get("comment");
// Apply changes
boolean changed = false;
if (newName != null && !newName.isEmpty() && !newName.equals(function.getName())) {
// Rename function
try {
TransactionHelper.executeInTransaction(program, "Rename Function", () -> {
function.setName(newName, ghidra.program.model.symbol.SourceType.USER_DEFINED);
return null;
});
changed = true;
} catch (Exception e) {
sendErrorResponse(exchange, 400, "Failed to rename function: " + e.getMessage(), "RENAME_FAILED");
return;
}
}
if (signature != null && !signature.isEmpty()) {
// Update function signature using our utility method
try {
boolean success = TransactionHelper.executeInTransaction(program, "Set Function Signature", () -> {
return GhidraUtil.setFunctionSignature(function, signature);
});
if (!success) {
sendErrorResponse(exchange, 400, "Failed to set function signature: invalid signature format", "SIGNATURE_FAILED");
return;
}
changed = true;
} catch (Exception e) {
sendErrorResponse(exchange, 400, "Failed to set function signature: " + e.getMessage(), "SIGNATURE_FAILED");
return;
}
}
if (comment != null) {
// Update comment
try {
TransactionHelper.executeInTransaction(program, "Set Function Comment", () -> {
function.setComment(comment);
return null;
});
changed = true;
} catch (Exception e) {
sendErrorResponse(exchange, 400, "Failed to set function comment: " + e.getMessage(), "COMMENT_FAILED");
return;
}
}
if (!changed) {
sendErrorResponse(exchange, 400, "No changes specified", "NO_CHANGES");
return;
}
// Return updated function with RESTful response structure
FunctionInfo info = buildFunctionInfo(function);
ResponseBuilder builder = new ResponseBuilder(exchange, port)
.success(true)
.result(info);
// Add HATEOAS links
builder.addLink("self", "/programs/current/functions/" + function.getEntryPoint());
builder.addLink("by_name", "/programs/current/functions/by-name/" + function.getName());
builder.addLink("program", "/programs/current");
sendJsonResponse(exchange, builder.build(), 200);
}
/**
* Handle DELETE requests to delete a function using the RESTful endpoint
*/
private void handleDeleteFunctionRESTful(HttpExchange exchange, Function function) throws IOException {
// Placeholder for function deletion
sendErrorResponse(exchange, 501, "Function deletion not implemented", "NOT_IMPLEMENTED");
}
/**
* Handle POST requests to create a new function using the RESTful endpoint
*/
private void handleCreateFunctionRESTful(HttpExchange exchange) throws IOException {
Program program = getCurrentProgram();
if (program == null) {
sendErrorResponse(exchange, 400, "No program is currently loaded", "NO_PROGRAM_LOADED");
return;
}
// Parse request body
Map<String, String> params = parseJsonPostParams(exchange);
String addressStr = params.get("address");
if (addressStr == null || addressStr.isEmpty()) {
sendErrorResponse(exchange, 400, "Missing address parameter", "MISSING_PARAMETER");
return;
}
// Get address
AddressFactory addressFactory = program.getAddressFactory();
Address address;
try {
address = addressFactory.getAddress(addressStr);
} catch (Exception e) {
sendErrorResponse(exchange, 400, "Invalid address format: " + addressStr, "INVALID_ADDRESS");
return;
}
if (address == null) {
sendErrorResponse(exchange, 400, "Invalid address: " + addressStr, "INVALID_ADDRESS");
return;
}
// Check if address is in a valid memory block
if (program.getMemory().getBlock(address) == null) {
sendErrorResponse(exchange, 400, "Address is not in a defined memory block: " + addressStr, "INVALID_ADDRESS");
return;
}
// Check if function already exists
if (program.getFunctionManager().getFunctionAt(address) != null) {
sendErrorResponse(exchange, 409, "Function already exists at address: " + addressStr, "FUNCTION_EXISTS");
return;
}
// Attempt to disassemble the code at the specified address before creating a function
try {
TransactionHelper.executeInTransaction(program, "Disassemble Before Function Creation", () -> {
// Check if there's already a defined instruction at the address
if (program.getListing().getInstructionAt(address) == null) {
// Attempt to directly disassemble at the address
try {
ghidra.app.cmd.disassemble.DisassembleCommand cmd =
new ghidra.app.cmd.disassemble.DisassembleCommand(address, null, true);
cmd.applyTo(program);
} catch (Exception ex) {
Msg.warn(this, "Basic disassembly failed: " + ex.getMessage());
}
}
return null;
});
} catch (Exception e) {
// Log the error but proceed with function creation attempt anyway
Msg.warn(this, "Disassembly before function creation failed: " + e.getMessage());
}
// Create function
Function function;
try {
function = TransactionHelper.executeInTransaction(program, "Create Function", () -> {
return program.getFunctionManager().createFunction(null, address, null, null);
});
} catch (Exception e) {
// If function creation initially fails, try a different approach
try {
Msg.info(this, "Initial function creation failed, attempting with code unit clearing");
// Clear any existing data at this location and try disassembling again
TransactionHelper.executeInTransaction(program, "Clear and Disassemble", () -> {
// Clear existing data at the address
program.getListing().clearCodeUnits(address, address, false);
// Try disassembling again
ghidra.app.cmd.disassemble.DisassembleCommand cmd =
new ghidra.app.cmd.disassemble.DisassembleCommand(address, null, true);
cmd.applyTo(program);
return null;
});
// Try creating the function again
function = TransactionHelper.executeInTransaction(program, "Create Function Retry", () -> {
return program.getFunctionManager().createFunction(null, address, null, null);
});
} catch (Exception e2) {
// Both attempts failed, return the error
sendErrorResponse(exchange, 400, "Failed to create function after multiple attempts: " + e.getMessage(), "CREATE_FAILED");
return;
}
}
if (function == null) {
sendErrorResponse(exchange, 500, "Failed to create function", "CREATE_FAILED");
return;
}
// Return created function with RESTful response structure
FunctionInfo info = buildFunctionInfo(function);
ResponseBuilder builder = new ResponseBuilder(exchange, port)
.success(true)
.result(info);
// Add HATEOAS links
builder.addLink("self", "/programs/current/functions/" + function.getEntryPoint());
builder.addLink("by_name", "/programs/current/functions/by-name/" + function.getName());
builder.addLink("program", "/programs/current");
builder.addLink("decompile", "/programs/current/functions/" + function.getEntryPoint() + "/decompile");
builder.addLink("disassembly", "/programs/current/functions/" + function.getEntryPoint() + "/disassembly");
sendJsonResponse(exchange, builder.build(), 201);
}
/**
* Handle requests to the /functions endpoint
*/
public void handleFunctions(HttpExchange exchange) throws IOException {
try {
// Always check for program availability first
Program program = getCurrentProgram();
if (program == null) {
sendErrorResponse(exchange, 503, "No program is currently loaded", "NO_PROGRAM_LOADED");
return;
}
if ("GET".equals(exchange.getRequestMethod())) {
Map<String, String> params = parseQueryParams(exchange);
int offset = parseIntOrDefault(params.get("offset"), 0);
int limit = parseIntOrDefault(params.get("limit"), 100);
String nameFilter = params.get("name");
String nameContainsFilter = params.get("name_contains");
String nameRegexFilter = params.get("name_matches_regex");
String addrFilter = params.get("addr");
List<Map<String, Object>> functions = new ArrayList<>();
// Get all functions
for (Function f : program.getFunctionManager().getFunctions(true)) {
// Apply filters
if (nameFilter != null && !f.getName().equals(nameFilter)) {
continue;
}
if (nameContainsFilter != null && !f.getName().toLowerCase().contains(nameContainsFilter.toLowerCase())) {
continue;
}
if (nameRegexFilter != null && !f.getName().matches(nameRegexFilter)) {
continue;
}
if (addrFilter != null && !f.getEntryPoint().toString().equals(addrFilter)) {
continue;
}
Map<String, Object> func = new HashMap<>();
func.put("name", f.getName());
func.put("address", f.getEntryPoint().toString());
// Add HATEOAS links (fixed to use proper URL paths)
Map<String, Object> links = new HashMap<>();
Map<String, String> selfLink = new HashMap<>();
selfLink.put("href", "/functions/" + f.getEntryPoint());
links.put("self", selfLink);
Map<String, String> programLink = new HashMap<>();
programLink.put("href", "/program");
links.put("program", programLink);
func.put("_links", links);
functions.add(func);
}
// Apply pagination
int endIndex = Math.min(functions.size(), offset + limit);
List<Map<String, Object>> paginatedFunctions = offset < functions.size()
? functions.subList(offset, endIndex)
: new ArrayList<>();
// Build response with pagination links
ResponseBuilder builder = new ResponseBuilder(exchange, port)
.success(true)
.result(paginatedFunctions);
// Add pagination metadata
Map<String, Object> metadata = new HashMap<>();
metadata.put("size", functions.size());
metadata.put("offset", offset);
metadata.put("limit", limit);
builder.metadata(metadata);
// Add HATEOAS links
builder.addLink("self", "/functions?offset=" + offset + "&limit=" + limit);
// Add next/prev links if applicable
if (endIndex < functions.size()) {
builder.addLink("next", "/functions?offset=" + endIndex + "&limit=" + limit);
}
if (offset > 0) {
int prevOffset = Math.max(0, offset - limit);
builder.addLink("prev", "/functions?offset=" + prevOffset + "&limit=" + limit);
}
// Add link to create a new function
builder.addLink("create", "/functions", "POST");
sendJsonResponse(exchange, builder.build(), 200);
} else if ("POST".equals(exchange.getRequestMethod())) {
// Create a new function
handleCreateFunction(exchange);
} else {
sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED");
}
} catch (Exception e) {
Msg.error(this, "Error handling /functions endpoint", e);
sendErrorResponse(exchange, 500, "Internal Server Error: " + e.getMessage(), "INTERNAL_ERROR");
}
}
/**
* Handle requests to the /functions/{name} endpoint
*/
private void handleFunction(HttpExchange exchange, String path) throws IOException {
try {
String functionName;
// If path is provided, use it; otherwise extract from the request URI
if (path != null && path.startsWith("/functions/")) {
functionName = path.substring("/functions/".length());
} else {
String requestPath = exchange.getRequestURI().getPath();
functionName = requestPath.substring("/functions/".length());
}
// Check for nested resources
if (functionName.contains("/")) {
handleFunctionResource(exchange, functionName);
return;
}
String method = exchange.getRequestMethod();
if ("GET".equals(method)) {
// Get function details
handleGetFunction(exchange, functionName);
} else if ("PATCH".equals(method)) {
// Update function
handleUpdateFunction(exchange, functionName);
} else if ("DELETE".equals(method)) {
// Delete function
handleDeleteFunction(exchange, functionName);
} else {
sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED");
}
} catch (Exception e) {
Msg.error(this, "Error handling /functions/{name} endpoint", e);
sendErrorResponse(exchange, 500, "Internal Server Error: " + e.getMessage(), "INTERNAL_ERROR");
}
}
/**
* Handle requests to the /functions/{name} endpoint derived from the path
*/
private void handleFunctionByPath(HttpExchange exchange) throws IOException {
try {
String path = exchange.getRequestURI().getPath();
String functionName = path.substring("/functions/".length());
// Check for nested resources
if (functionName.contains("/")) {
handleFunctionResource(exchange, functionName);
return;
}
String method = exchange.getRequestMethod();
if ("GET".equals(method)) {
// Get function details
handleGetFunction(exchange, functionName);
} else if ("PATCH".equals(method)) {
// Update function
handleUpdateFunction(exchange, functionName);
} else if ("DELETE".equals(method)) {
// Delete function
handleDeleteFunction(exchange, functionName);
} else {
sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED");
}
} catch (Exception e) {
Msg.error(this, "Error handling /functions/{name} endpoint", e);
sendErrorResponse(exchange, 500, "Internal Server Error: " + e.getMessage(), "INTERNAL_ERROR");
}
}
/**
* Handle requests to function resources like /functions/{name}/decompile
*/
private void handleFunctionResource(HttpExchange exchange, String functionIdent, String resource) throws IOException {
Function function = null;
// Try to find function by address first
function = findFunctionByAddress(functionIdent);
// If not found by address, try by name
if (function == null) {
function = findFunctionByName(functionIdent);
}
if (function == null) {
sendErrorResponse(exchange, 404, "Function not found: " + functionIdent, "FUNCTION_NOT_FOUND");
return;
}
if (resource.equals("decompile")) {
handleDecompileFunction(exchange, function);
} else if (resource.equals("disassembly")) {
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");
}
}
private void handleFunctionResource(HttpExchange exchange, String functionPath) throws IOException {
int slashIndex = functionPath.indexOf('/');
if (slashIndex == -1) {
sendErrorResponse(exchange, 404, "Invalid function resource path: " + functionPath, "RESOURCE_NOT_FOUND");
return;
}
String functionIdent = functionPath.substring(0, slashIndex);
String resource = functionPath.substring(slashIndex + 1);
handleFunctionResource(exchange, functionIdent, resource);
}
/**
* Handle GET requests to get function details
*/
public void handleGetFunction(HttpExchange exchange, String functionName) throws IOException {
Program program = getCurrentProgram();
if (program == null) {
sendErrorResponse(exchange, 503, "No program is currently loaded", "NO_PROGRAM_LOADED");
return;
}
Function function = findFunctionByName(functionName);
if (function == null) {
sendErrorResponse(exchange, 404, "Function not found: " + functionName, "FUNCTION_NOT_FOUND");
return;
}
// Build function info
FunctionInfo info = buildFunctionInfo(function);
// Build response with HATEOAS links
ResponseBuilder builder = new ResponseBuilder(exchange, port)
.success(true)
.result(info);
// Add HATEOAS links
builder.addLink("self", "/functions/" + functionName);
builder.addLink("program", "/programs/current");
builder.addLink("decompile", "/functions/" + functionName + "/decompile");
builder.addLink("disassembly", "/functions/" + functionName + "/disassembly");
builder.addLink("variables", "/functions/" + functionName + "/variables");
// Add xrefs links
builder.addLink("xrefs_to", "/programs/current/xrefs?to_addr=" + function.getEntryPoint().toString());
builder.addLink("xrefs_from", "/programs/current/xrefs?from_addr=" + function.getEntryPoint().toString());
sendJsonResponse(exchange, builder.build(), 200);
}
/**
* Handle PATCH requests to update a function
*/
private void handleUpdateFunction(HttpExchange exchange, String functionName) throws IOException {
Program program = getCurrentProgram();
if (program == null) {
sendErrorResponse(exchange, 503, "No program is currently loaded", "NO_PROGRAM_LOADED");
return;
}
Function function = findFunctionByName(functionName);
if (function == null) {
sendErrorResponse(exchange, 404, "Function not found: " + functionName, "FUNCTION_NOT_FOUND");
return;
}
// Parse request body
Map<String, String> params = parseJsonPostParams(exchange);
String newName = params.get("name");
String signature = params.get("signature");
String comment = params.get("comment");
// Apply changes
boolean changed = false;
if (newName != null && !newName.isEmpty() && !newName.equals(function.getName())) {
// Rename function
try {
TransactionHelper.executeInTransaction(program, "Rename Function", () -> {
function.setName(newName, ghidra.program.model.symbol.SourceType.USER_DEFINED);
return null;
});
changed = true;
} catch (Exception e) {
sendErrorResponse(exchange, 400, "Failed to rename function: " + e.getMessage(), "RENAME_FAILED");
return;
}
}
if (signature != null && !signature.isEmpty()) {
// Update function signature using our utility method
try {
boolean success = TransactionHelper.executeInTransaction(program, "Set Function Signature", () -> {
return GhidraUtil.setFunctionSignature(function, signature);
});
if (!success) {
sendErrorResponse(exchange, 400, "Failed to set function signature: invalid signature format", "SIGNATURE_FAILED");
return;
}
changed = true;
} catch (Exception e) {
sendErrorResponse(exchange, 400, "Failed to set function signature: " + e.getMessage(), "SIGNATURE_FAILED");
return;
}
}
if (comment != null) {
// Update comment
try {
TransactionHelper.executeInTransaction(program, "Set Function Comment", () -> {
function.setComment(comment);
return null;
});
changed = true;
} catch (Exception e) {
sendErrorResponse(exchange, 400, "Failed to set function comment: " + e.getMessage(), "COMMENT_FAILED");
return;
}
}
if (!changed) {
sendErrorResponse(exchange, 400, "No changes specified", "NO_CHANGES");
return;
}
// Return updated function
FunctionInfo info = buildFunctionInfo(function);
ResponseBuilder builder = new ResponseBuilder(exchange, port)
.success(true)
.result(info);
// Add HATEOAS links
builder.addLink("self", "/functions/" + function.getName());
sendJsonResponse(exchange, builder.build(), 200);
}
/**
* Handle DELETE requests to delete a function
*/
private void handleDeleteFunction(HttpExchange exchange, String functionName) throws IOException {
// This is a placeholder - actual implementation would delete the function
sendErrorResponse(exchange, 501, "Function deletion not implemented", "NOT_IMPLEMENTED");
}
/**
* Handle POST requests to create a new function
*/
private void handleCreateFunction(HttpExchange exchange) throws IOException {
Program program = getCurrentProgram();
if (program == null) {
sendErrorResponse(exchange, 503, "No program is currently loaded", "NO_PROGRAM_LOADED");
return;
}
// Parse request body
Map<String, String> params = parseJsonPostParams(exchange);
String addressStr = params.get("address");
if (addressStr == null || addressStr.isEmpty()) {
sendErrorResponse(exchange, 400, "Missing address parameter", "MISSING_PARAMETER");
return;
}
// Get address
AddressFactory addressFactory = program.getAddressFactory();
Address address;
try {
address = addressFactory.getAddress(addressStr);
} catch (Exception e) {
sendErrorResponse(exchange, 400, "Invalid address format: " + addressStr, "INVALID_ADDRESS");
return;
}
if (address == null) {
sendErrorResponse(exchange, 400, "Invalid address: " + addressStr, "INVALID_ADDRESS");
return;
}
// Check if function already exists
if (program.getFunctionManager().getFunctionAt(address) != null) {
sendErrorResponse(exchange, 409, "Function already exists at address: " + addressStr, "FUNCTION_EXISTS");
return;
}
// Attempt to disassemble the code at the specified address before creating a function
try {
TransactionHelper.executeInTransaction(program, "Disassemble Before Function Creation", () -> {
// Check if there's already a defined instruction at the address
if (program.getListing().getInstructionAt(address) == null) {
// Attempt to directly disassemble at the address
try {
ghidra.app.cmd.disassemble.DisassembleCommand cmd =
new ghidra.app.cmd.disassemble.DisassembleCommand(address, null, true);
cmd.applyTo(program);
} catch (Exception ex) {
Msg.warn(this, "Basic disassembly failed: " + ex.getMessage());
}
}
return null;
});
} catch (Exception e) {
// Log the error but proceed with function creation attempt anyway
Msg.warn(this, "Disassembly before function creation failed: " + e.getMessage());
}
// Create function
Function function;
try {
function = TransactionHelper.executeInTransaction(program, "Create Function", () -> {
return program.getFunctionManager().createFunction(null, address, null, null);
});
} catch (Exception e) {
// If function creation initially fails, try a different approach
try {
Msg.info(this, "Initial function creation failed, attempting with code unit clearing");
// Clear any existing data at this location and try disassembling again
TransactionHelper.executeInTransaction(program, "Clear and Disassemble", () -> {
// Clear existing data at the address
program.getListing().clearCodeUnits(address, address, false);
// Try disassembling again
ghidra.app.cmd.disassemble.DisassembleCommand cmd =
new ghidra.app.cmd.disassemble.DisassembleCommand(address, null, true);
cmd.applyTo(program);
return null;
});
// Try creating the function again
function = TransactionHelper.executeInTransaction(program, "Create Function Retry", () -> {
return program.getFunctionManager().createFunction(null, address, null, null);
});
} catch (Exception e2) {
// Both attempts failed, return the error
sendErrorResponse(exchange, 400, "Failed to create function after multiple attempts: " + e.getMessage(), "CREATE_FAILED");
return;
}
}
if (function == null) {
sendErrorResponse(exchange, 500, "Failed to create function", "CREATE_FAILED");
return;
}
// Return created function
FunctionInfo info = buildFunctionInfo(function);
ResponseBuilder builder = new ResponseBuilder(exchange, port)
.success(true)
.result(info);
// Add HATEOAS links
builder.addLink("self", "/functions/" + function.getName());
sendJsonResponse(exchange, builder.build(), 201);
}
/**
* Handle requests to decompile a function
*/
public void handleDecompileFunction(HttpExchange exchange, Function function) throws IOException {
if ("GET".equals(exchange.getRequestMethod())) {
Map<String, String> params = parseQueryParams(exchange);
boolean syntaxTree = Boolean.parseBoolean(params.getOrDefault("syntax_tree", "false"));
String style = params.getOrDefault("style", "normalize");
String format = params.getOrDefault("format", "structured");
int timeout = parseIntOrDefault(params.get("timeout"), 30);
// Decompile function
String decompilation = GhidraUtil.decompileFunction(function);
// Create function info
Map<String, Object> functionInfo = new HashMap<>();
functionInfo.put("address", function.getEntryPoint().toString());
functionInfo.put("name", function.getName());
// Create the result structure according to GHIDRA_HTTP_API.md
Map<String, Object> result = new HashMap<>();
result.put("function", functionInfo);
result.put("decompiled", decompilation != null ? decompilation : "// Decompilation failed");
// Add syntax tree if requested
if (syntaxTree) {
result.put("syntax_tree", "Syntax tree not implemented");
}
ResponseBuilder builder = new ResponseBuilder(exchange, port)
.success(true)
.result(result);
// Path for links (updated to use the correct paths)
String functionPath = "/functions/" + function.getEntryPoint().toString();
// Add HATEOAS links
builder.addLink("self", functionPath + "/decompile");
builder.addLink("function", functionPath);
builder.addLink("disassembly", functionPath + "/disassembly");
builder.addLink("variables", functionPath + "/variables");
builder.addLink("program", "/program");
sendJsonResponse(exchange, builder.build(), 200);
} else {
sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED");
}
}
/**
* Handle requests to disassemble a function
*/
public void handleDisassembleFunction(HttpExchange exchange, Function function) throws IOException {
if ("GET".equals(exchange.getRequestMethod())) {
List<Map<String, Object>> disassembly = new ArrayList<>();
Program program = function.getProgram();
if (program != null) {
try {
// Get actual disassembly from the program
Address startAddr = function.getEntryPoint();
Address endAddr = function.getBody().getMaxAddress();
ghidra.program.model.listing.Listing listing = program.getListing();
ghidra.program.model.listing.InstructionIterator instrIter =
listing.getInstructions(startAddr, true);
while (instrIter.hasNext() && disassembly.size() < 100) {
ghidra.program.model.listing.Instruction instr = instrIter.next();
// Stop if we've gone past the end of the function
if (instr.getAddress().compareTo(endAddr) > 0) {
break;
}
Map<String, Object> instrMap = new HashMap<>();
instrMap.put("address", instr.getAddress().toString());
// Get actual bytes
byte[] bytes = new byte[instr.getLength()];
program.getMemory().getBytes(instr.getAddress(), bytes);
StringBuilder hexBytes = new StringBuilder();
for (byte b : bytes) {
hexBytes.append(String.format("%02X", b & 0xFF));
}
instrMap.put("bytes", hexBytes.toString());
// Get mnemonic and operands
instrMap.put("mnemonic", instr.getMnemonicString());
instrMap.put("operands", instr.toString().substring(instr.getMnemonicString().length()).trim());
disassembly.add(instrMap);
}
} catch (Exception e) {
Msg.error(this, "Error getting disassembly for function: " + function.getName(), e);
}
// If we couldn't get real instructions, add placeholder
if (disassembly.isEmpty()) {
Address addr = function.getEntryPoint();
for (int i = 0; i < 5; i++) {
Map<String, Object> instruction = new HashMap<>();
instruction.put("address", addr.toString());
instruction.put("mnemonic", "???");
instruction.put("operands", "???");
instruction.put("bytes", "????");
disassembly.add(instruction);
addr = addr.add(2);
}
}
}
Map<String, Object> functionInfo = new HashMap<>();
functionInfo.put("address", function.getEntryPoint().toString());
functionInfo.put("name", function.getName());
functionInfo.put("signature", function.getSignature().toString());
Map<String, Object> result = new HashMap<>();
result.put("function", functionInfo);
result.put("instructions", disassembly);
ResponseBuilder builder = new ResponseBuilder(exchange, port)
.success(true)
.result(result);
// Update to use the correct paths
String functionPath = "/functions/" + function.getEntryPoint().toString();
builder.addLink("self", functionPath + "/disassembly");
builder.addLink("function", functionPath);
builder.addLink("decompile", functionPath + "/decompile");
builder.addLink("variables", functionPath + "/variables");
builder.addLink("program", "/program");
sendJsonResponse(exchange, builder.build(), 200);
} else {
sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED");
}
}
/**
* Handle requests to get function variables
*/
public void handleFunctionVariables(HttpExchange exchange, Function function) throws IOException {
if ("GET".equals(exchange.getRequestMethod())) {
List<Map<String, Object>> variables = GhidraUtil.getFunctionVariables(function);
Map<String, Object> functionInfo = new HashMap<>();
functionInfo.put("address", function.getEntryPoint().toString());
functionInfo.put("name", function.getName());
if (function.getReturnType() != null) {
functionInfo.put("returnType", function.getReturnType().getName());
}
if (function.getCallingConventionName() != null) {
functionInfo.put("callingConvention", function.getCallingConventionName());
}
Map<String, Object> result = new HashMap<>();
result.put("function", functionInfo);
result.put("variables", variables);
// Update to use the correct paths
String functionPath = "/functions/" + function.getEntryPoint().toString();
String functionByNamePath = "/functions/by-name/" + function.getName();
ResponseBuilder builder = new ResponseBuilder(exchange, port)
.success(true)
.result(result);
builder.addLink("self", functionPath + "/variables");
builder.addLink("function", functionPath);
builder.addLink("by_name", functionByNamePath);
builder.addLink("decompile", functionPath + "/decompile");
builder.addLink("disassembly", functionPath + "/disassembly");
builder.addLink("program", "/program");
sendJsonResponse(exchange, builder.build(), 200);
} else if ("PATCH".equals(exchange.getRequestMethod())) {
String path = exchange.getRequestURI().getPath();
if (path.contains("/variables/")) {
String variableName = path.substring(path.lastIndexOf('/') + 1);
handleUpdateVariable(exchange, function, variableName);
} else {
sendErrorResponse(exchange, 400, "Missing variable name", "MISSING_PARAMETER");
}
} else {
sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED");
}
}
/**
* Handle requests to update a function variable
*/
private void handleUpdateVariable(HttpExchange exchange, Function function, String variableName) throws IOException {
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;
}
// Use transaction to update variable
Program program = getCurrentProgram();
if (program == null) {
sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED");
return;
}
boolean success = TransactionHelper.executeInTransaction(program, "Update Variable", () -> {
try {
// 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
for (Iterator<HighSymbol> symbolIter = highFunc.getLocalSymbolMap().getSymbols(); symbolIter.hasNext();) {
HighSymbol symbol = symbolIter.next();
if (symbol.getName().equals(variableName)) {
// Rename the variable using HighFunctionDBUtil
HighFunctionDBUtil.updateDBVariable(symbol, newName, null, SourceType.USER_DEFINED);
return true;
}
}
}
}
} finally {
decomp.dispose();
}
return false;
} catch (Exception e) {
Msg.error(this, "Error updating variable: " + e.getMessage(), e);
return false;
}
});
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", "Variable renamed successfully");
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");
}
}
/**
* Helper method to find a function by name
*/
private Function findFunctionByName(String name) {
Program program = getCurrentProgram();
if (program == null) {
return null;
}
for (Function f : program.getFunctionManager().getFunctions(true)) {
if (f.getName().equals(name)) {
return f;
}
}
return null;
}
private Function findFunctionByAddress(String addressString) {
Program program = getCurrentProgram();
if (program == null) {
return null;
}
try {
ghidra.program.model.address.Address address = program.getAddressFactory().getAddress(addressString);
return program.getFunctionManager().getFunctionAt(address);
} catch (Exception e) {
return null;
}
}
/**
* Helper method to build a FunctionInfo object from a Function
*/
private FunctionInfo buildFunctionInfo(Function function) {
FunctionInfo.Builder builder = FunctionInfo.builder()
.name(function.getName())
.address(function.getEntryPoint().toString())
.signature(function.getSignature().getPrototypeString());
// Add return type
if (function.getReturnType() != null) {
builder.returnType(function.getReturnType().getName());
}
// Add calling convention
if (function.getCallingConventionName() != null) {
builder.callingConvention(function.getCallingConventionName());
}
// Add namespace
if (function.getParentNamespace() != null) {
builder.namespace(function.getParentNamespace().getName());
}
// Add external flag
builder.isExternal(function.isExternal());
// Add parameters
for (int i = 0; i < function.getParameterCount(); i++) {
ghidra.program.model.listing.Parameter param = function.getParameter(i);
FunctionInfo.ParameterInfo paramInfo = FunctionInfo.ParameterInfo.builder()
.name(param.getName())
.dataType(param.getDataType().getName())
.ordinal(i)
.storage(param.getRegister() != null ? param.getRegister().getName() : "stack")
.build();
builder.addParameter(paramInfo);
}
return builder.build();
}
}