WIP big refactor
This commit is contained in:
parent
454c73908c
commit
9879e71e88
14
CHANGELOG.md
14
CHANGELOG.md
@ -17,12 +17,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
- Multiple simplification styles
|
||||
- Comprehensive API documentation (GHIDRA_HTTP_API.md, MCP_BRIDGE_API.md)
|
||||
- Standardized JSON response formats
|
||||
- Implemented `/plugin-version` endpoint for version checking
|
||||
- Added proper error handling for when no program is loaded
|
||||
|
||||
### Changed
|
||||
- Unified all endpoints to use structured JSON
|
||||
- Improved error handling and response metadata
|
||||
- Simplified bridge code and added type hints
|
||||
- Updated port discovery to use DEFAULT_GHIDRA_PORT
|
||||
- Refactored Java plugin into modular architecture:
|
||||
- Separated concerns into api, endpoints, util, and model packages
|
||||
- Created standardized response builders and error handlers
|
||||
- Implemented transaction management helpers
|
||||
- Added model classes for structured data representation
|
||||
- Removed `port` field from responses (bridge knows what instance it called)
|
||||
|
||||
### Fixed
|
||||
- Fixed endpoint registration in refactored code (all endpoints now working)
|
||||
- Improved handling of program-dependent endpoints when no program is loaded
|
||||
- Enhanced root endpoint to dynamically include links to available endpoints
|
||||
- Added proper HATEOAS links to all endpoints
|
||||
|
||||
## [1.4.0] - 2025-04-08
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
8
src/main/java/eu/starsong/ghidra/api/ApiConstants.java
Normal file
8
src/main/java/eu/starsong/ghidra/api/ApiConstants.java
Normal file
@ -0,0 +1,8 @@
|
||||
package eu.starsong.ghidra.api;
|
||||
|
||||
public class ApiConstants {
|
||||
public static final String PLUGIN_VERSION = "v1.0.0";
|
||||
public static final int API_VERSION = 1;
|
||||
public static final int DEFAULT_PORT = 8192;
|
||||
public static final int MAX_PORT_ATTEMPTS = 10;
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package eu.starsong.ghidra.api;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
|
||||
public interface GhidraJsonEndpoint extends HttpHandler {
|
||||
void registerEndpoints(com.sun.net.httpserver.HttpServer server);
|
||||
}
|
||||
73
src/main/java/eu/starsong/ghidra/api/ResponseBuilder.java
Normal file
73
src/main/java/eu/starsong/ghidra/api/ResponseBuilder.java
Normal file
@ -0,0 +1,73 @@
|
||||
package eu.starsong.ghidra.api;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Builder for standardized API responses (following GHIDRA_HTTP_API.md v1).
|
||||
* This should be used by endpoint handlers to construct responses.
|
||||
*/
|
||||
public class ResponseBuilder {
|
||||
private final HttpExchange exchange;
|
||||
private final int port; // Port of the current Ghidra instance handling the request
|
||||
private JsonObject response;
|
||||
private JsonObject links; // For HATEOAS links
|
||||
private final Gson gson = new Gson(); // Gson instance for serialization
|
||||
|
||||
public ResponseBuilder(HttpExchange exchange, int port) {
|
||||
this.exchange = exchange;
|
||||
this.port = port;
|
||||
this.response = new JsonObject();
|
||||
this.links = new JsonObject();
|
||||
|
||||
// Add standard fields
|
||||
String requestId = exchange.getRequestHeaders().getFirst("X-Request-ID");
|
||||
response.addProperty("id", requestId != null ? requestId : UUID.randomUUID().toString());
|
||||
response.addProperty("instance", "http://localhost:" + port); // URL of this instance
|
||||
}
|
||||
|
||||
public ResponseBuilder success(boolean success) {
|
||||
response.addProperty("success", success);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder result(Object data) {
|
||||
response.add("result", gson.toJsonTree(data));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder error(String message, String code) {
|
||||
JsonObject error = new JsonObject();
|
||||
error.addProperty("message", message);
|
||||
if (code != null) {
|
||||
error.addProperty("code", code);
|
||||
}
|
||||
response.add("error", error);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResponseBuilder addLink(String rel, String href) {
|
||||
JsonObject link = new JsonObject();
|
||||
link.addProperty("href", href);
|
||||
links.add(rel, link);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Overload to add link with method
|
||||
public ResponseBuilder addLink(String rel, String href, String method) {
|
||||
JsonObject link = new JsonObject();
|
||||
link.addProperty("href", href);
|
||||
link.addProperty("method", method);
|
||||
links.add(rel, link);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonObject build() {
|
||||
if (links.size() > 0) {
|
||||
response.add("_links", links);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
package eu.starsong.ghidra.endpoints;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import eu.starsong.ghidra.api.GhidraJsonEndpoint;
|
||||
import eu.starsong.ghidra.api.ResponseBuilder; // Import ResponseBuilder
|
||||
import eu.starsong.ghidra.util.GhidraUtil; // Import GhidraUtil
|
||||
import eu.starsong.ghidra.util.HttpUtil; // Import HttpUtil
|
||||
import ghidra.program.model.listing.Program;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class AbstractEndpoint implements GhidraJsonEndpoint {
|
||||
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
// This method is required by HttpHandler interface
|
||||
// Each endpoint will register its own context handlers with specific paths
|
||||
// so this default implementation should never be called
|
||||
sendErrorResponse(exchange, 404, "Endpoint not found", "ENDPOINT_NOT_FOUND");
|
||||
}
|
||||
|
||||
protected final Gson gson = new Gson(); // Keep Gson if needed for specific object handling
|
||||
protected Program currentProgram;
|
||||
protected int port; // Add port field
|
||||
|
||||
// Constructor to receive Program and Port
|
||||
public AbstractEndpoint(Program program, int port) {
|
||||
this.currentProgram = program;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
// Simplified getCurrentProgram - assumes constructor sets it
|
||||
protected Program getCurrentProgram() {
|
||||
return currentProgram;
|
||||
}
|
||||
|
||||
// --- Methods using HttpUtil ---
|
||||
|
||||
protected void sendJsonResponse(HttpExchange exchange, JsonObject data, int statusCode) throws IOException {
|
||||
HttpUtil.sendJsonResponse(exchange, data, statusCode, this.port);
|
||||
}
|
||||
|
||||
// Overload for sending success responses easily using ResponseBuilder
|
||||
protected void sendSuccessResponse(HttpExchange exchange, Object resultData) throws IOException {
|
||||
// Check if program is required but not available
|
||||
if (currentProgram == null && requiresProgram()) {
|
||||
sendErrorResponse(exchange, 503, "No program is currently loaded", "NO_PROGRAM_LOADED");
|
||||
return;
|
||||
}
|
||||
|
||||
ResponseBuilder builder = new ResponseBuilder(exchange, port)
|
||||
.success(true)
|
||||
.result(resultData);
|
||||
// Add common links if desired here
|
||||
HttpUtil.sendJsonResponse(exchange, builder.build(), 200, this.port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method in endpoint implementations that require a program to function.
|
||||
* @return true if this endpoint requires a program, false otherwise
|
||||
*/
|
||||
protected boolean requiresProgram() {
|
||||
// Default implementation returns true for most endpoints
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void sendErrorResponse(HttpExchange exchange, int code, String message, String errorCode) throws IOException {
|
||||
HttpUtil.sendErrorResponse(exchange, code, message, errorCode, this.port);
|
||||
}
|
||||
|
||||
// Overload without error code
|
||||
protected void sendErrorResponse(HttpExchange exchange, int code, String message) throws IOException {
|
||||
HttpUtil.sendErrorResponse(exchange, code, message, null, this.port);
|
||||
}
|
||||
|
||||
protected Map<String, String> parseQueryParams(HttpExchange exchange) {
|
||||
return HttpUtil.parseQueryParams(exchange);
|
||||
}
|
||||
|
||||
protected Map<String, String> parseJsonPostParams(HttpExchange exchange) throws IOException {
|
||||
return HttpUtil.parseJsonPostParams(exchange);
|
||||
}
|
||||
|
||||
// --- Methods using GhidraUtil ---
|
||||
|
||||
protected int parseIntOrDefault(String val, int defaultValue) {
|
||||
return GhidraUtil.parseIntOrDefault(val, defaultValue);
|
||||
}
|
||||
|
||||
// Add other common Ghidra related utilities here or call GhidraUtil directly
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
package eu.starsong.ghidra.endpoints;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.symbol.Namespace;
|
||||
import ghidra.program.model.symbol.Symbol;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
public class ClassEndpoints extends AbstractEndpoint {
|
||||
|
||||
// Updated constructor to accept port
|
||||
public ClassEndpoints(Program program, int port) {
|
||||
super(program, port); // Call super constructor
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerEndpoints(HttpServer server) {
|
||||
server.createContext("/classes", this::handleClasses);
|
||||
}
|
||||
|
||||
private void handleClasses(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
Map<String, String> qparams = parseQueryParams(exchange);
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0); // Inherited
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 100); // Inherited
|
||||
Object resultData = getAllClassNames(offset, limit);
|
||||
// Check if helper returned an error object
|
||||
if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) {
|
||||
sendJsonResponse(exchange, (JsonObject)resultData, 400); // Use base sendJsonResponse
|
||||
} else {
|
||||
sendSuccessResponse(exchange, resultData); // Use success helper
|
||||
}
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed"); // Inherited
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in /classes endpoint", e);
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); // Inherited
|
||||
}
|
||||
}
|
||||
|
||||
// --- Method moved from GhydraMCPPlugin ---
|
||||
|
||||
private JsonObject getAllClassNames(int offset, int limit) {
|
||||
if (currentProgram == null) {
|
||||
return createErrorResponse("No program loaded", 400);
|
||||
}
|
||||
|
||||
Set<String> classNames = new HashSet<>();
|
||||
for (Symbol symbol : currentProgram.getSymbolTable().getAllSymbols(true)) {
|
||||
Namespace ns = symbol.getParentNamespace();
|
||||
// Check if namespace is not null, not global, and represents a class
|
||||
if (ns != null && !ns.isGlobal() && ns.getSymbol().getSymbolType().isNamespace()) {
|
||||
// Basic check, might need refinement based on how classes are represented
|
||||
classNames.add(ns.getName(true)); // Get fully qualified name
|
||||
}
|
||||
}
|
||||
|
||||
List<String> sorted = new ArrayList<>(classNames);
|
||||
Collections.sort(sorted);
|
||||
|
||||
int start = Math.max(0, offset);
|
||||
int end = Math.min(sorted.size(), offset + limit);
|
||||
List<String> paginated = sorted.subList(start, end);
|
||||
|
||||
return createSuccessResponse(paginated); // Keep internal helper for now
|
||||
}
|
||||
|
||||
// --- Helper Methods (Keep internal for now) ---
|
||||
|
||||
private JsonObject createSuccessResponse(Object resultData) {
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("success", true);
|
||||
response.add("result", gson.toJsonTree(resultData));
|
||||
return response;
|
||||
}
|
||||
|
||||
private JsonObject createErrorResponse(String errorMessage, int statusCode) {
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("success", false);
|
||||
response.addProperty("error", errorMessage);
|
||||
response.addProperty("status_code", statusCode);
|
||||
return response;
|
||||
}
|
||||
|
||||
// parseIntOrDefault is inherited from AbstractEndpoint
|
||||
}
|
||||
192
src/main/java/eu/starsong/ghidra/endpoints/DataEndpoints.java
Normal file
192
src/main/java/eu/starsong/ghidra/endpoints/DataEndpoints.java
Normal file
@ -0,0 +1,192 @@
|
||||
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.util.TransactionHelper;
|
||||
import eu.starsong.ghidra.util.TransactionHelper.TransactionException;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.Data;
|
||||
import ghidra.program.model.listing.DataIterator;
|
||||
import ghidra.program.model.listing.Listing;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.mem.MemoryBlock;
|
||||
import ghidra.program.model.symbol.SourceType;
|
||||
import ghidra.program.model.symbol.Symbol;
|
||||
import ghidra.program.model.symbol.SymbolTable;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
public class DataEndpoints extends AbstractEndpoint {
|
||||
|
||||
// Updated constructor to accept port
|
||||
public DataEndpoints(Program program, int port) {
|
||||
super(program, port); // Call super constructor
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerEndpoints(HttpServer server) {
|
||||
server.createContext("/data", this::handleData);
|
||||
}
|
||||
|
||||
private void handleData(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
handleListData(exchange);
|
||||
} else if ("POST".equals(exchange.getRequestMethod())) {
|
||||
handleRenameData(exchange);
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in /data endpoint", e);
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleListData(HttpExchange exchange) throws IOException {
|
||||
Map<String, String> qparams = parseQueryParams(exchange);
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0); // Inherited
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 100); // Inherited
|
||||
Object resultData = listDefinedData(offset, limit);
|
||||
// Check if helper returned an error object
|
||||
if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) {
|
||||
sendJsonResponse(exchange, (JsonObject)resultData, 400); // Use base sendJsonResponse
|
||||
} else {
|
||||
sendSuccessResponse(exchange, resultData); // Use success helper
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRenameData(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
Map<String, String> params = parseJsonPostParams(exchange);
|
||||
final String addressStr = params.get("address");
|
||||
final String newName = params.get("newName");
|
||||
|
||||
if (addressStr == null || addressStr.isEmpty() || newName == null || newName.isEmpty()) {
|
||||
sendErrorResponse(exchange, 400, "Missing required parameters: address, newName"); // Inherited
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentProgram == null) {
|
||||
sendErrorResponse(exchange, 400, "No program loaded"); // Inherited
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
TransactionHelper.executeInTransaction(currentProgram, "Rename Data", () -> {
|
||||
if (!renameDataAtAddress(addressStr, newName)) {
|
||||
throw new Exception("Rename data operation failed internally.");
|
||||
}
|
||||
return null; // Return null for void operation
|
||||
});
|
||||
// Use sendSuccessResponse for consistency
|
||||
sendSuccessResponse(exchange, Map.of("message", "Data renamed successfully"));
|
||||
} catch (TransactionException e) {
|
||||
Msg.error(this, "Transaction failed: Rename Data", e);
|
||||
// Use inherited sendErrorResponse
|
||||
sendErrorResponse(exchange, 500, "Failed to rename data: " + e.getMessage(), "TRANSACTION_ERROR");
|
||||
} catch (Exception e) { // Catch potential AddressFormatException or other issues
|
||||
Msg.error(this, "Error during rename data operation", e);
|
||||
// Use inherited sendErrorResponse
|
||||
sendErrorResponse(exchange, 400, "Error renaming data: " + e.getMessage(), "INVALID_PARAMETER");
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
Msg.error(this, "Error parsing POST params for data rename", e);
|
||||
sendErrorResponse(exchange, 400, "Invalid request body: " + e.getMessage(), "INVALID_REQUEST"); // Inherited
|
||||
} catch (Exception e) { // Catch unexpected errors
|
||||
Msg.error(this, "Unexpected error renaming data", e);
|
||||
sendErrorResponse(exchange, 500, "Error renaming data: " + e.getMessage(), "INTERNAL_ERROR"); // Inherited
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Methods moved from GhydraMCPPlugin ---
|
||||
|
||||
private JsonObject listDefinedData(int offset, int limit) {
|
||||
if (currentProgram == null) {
|
||||
return createErrorResponse("No program loaded", 400);
|
||||
}
|
||||
|
||||
List<Map<String, String>> dataItems = new ArrayList<>();
|
||||
for (MemoryBlock block : currentProgram.getMemory().getBlocks()) {
|
||||
DataIterator it = currentProgram.getListing().getDefinedData(block.getStart(), true);
|
||||
while (it.hasNext()) {
|
||||
Data data = it.next();
|
||||
if (block.contains(data.getAddress())) {
|
||||
Map<String, String> item = new HashMap<>();
|
||||
item.put("address", data.getAddress().toString());
|
||||
item.put("label", data.getLabel() != null ? data.getLabel() : "(unnamed)");
|
||||
item.put("value", data.getDefaultValueRepresentation());
|
||||
item.put("dataType", data.getDataType().getName());
|
||||
dataItems.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
int start = Math.max(0, offset);
|
||||
int end = Math.min(dataItems.size(), offset + limit);
|
||||
List<Map<String, String>> paginated = dataItems.subList(start, end);
|
||||
|
||||
return createSuccessResponse(paginated);
|
||||
}
|
||||
|
||||
private boolean renameDataAtAddress(String addressStr, String newName) throws Exception {
|
||||
// This method now throws Exception to be caught by the transaction helper
|
||||
AtomicBoolean successFlag = new AtomicBoolean(false);
|
||||
try {
|
||||
Address addr = currentProgram.getAddressFactory().getAddress(addressStr);
|
||||
Listing listing = currentProgram.getListing();
|
||||
Data data = listing.getDefinedDataAt(addr);
|
||||
if (data != null) {
|
||||
SymbolTable symTable = currentProgram.getSymbolTable();
|
||||
Symbol symbol = symTable.getPrimarySymbol(addr);
|
||||
if (symbol != null) {
|
||||
symbol.setName(newName, SourceType.USER_DEFINED);
|
||||
successFlag.set(true);
|
||||
} else {
|
||||
// Create a new label if no primary symbol exists
|
||||
symTable.createLabel(addr, newName, SourceType.USER_DEFINED);
|
||||
successFlag.set(true);
|
||||
}
|
||||
} else {
|
||||
throw new Exception("No defined data found at address: " + addressStr);
|
||||
}
|
||||
} catch (ghidra.program.model.address.AddressFormatException afe) {
|
||||
throw new Exception("Invalid address format: " + addressStr, afe);
|
||||
} catch (ghidra.util.exception.InvalidInputException iie) {
|
||||
throw new Exception("Invalid name: " + newName, iie);
|
||||
} catch (Exception e) { // Catch other potential Ghidra exceptions
|
||||
throw new Exception("Failed to rename data at " + addressStr, e);
|
||||
}
|
||||
return successFlag.get();
|
||||
}
|
||||
|
||||
|
||||
// --- Helper Methods (Keep internal for now, refactor later if needed) ---
|
||||
// Note: These might differ slightly from AbstractEndpoint/ResponseBuilder, review needed.
|
||||
|
||||
private JsonObject createSuccessResponse(Object resultData) {
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("success", true);
|
||||
response.add("result", gson.toJsonTree(resultData));
|
||||
return response;
|
||||
}
|
||||
|
||||
private JsonObject createErrorResponse(String errorMessage, int statusCode) {
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("success", false);
|
||||
response.addProperty("error", errorMessage);
|
||||
response.addProperty("status_code", statusCode);
|
||||
return response;
|
||||
}
|
||||
|
||||
// parseIntOrDefault is inherited from AbstractEndpoint
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
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.util.TransactionHelper;
|
||||
import ghidra.program.model.listing.Function;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.io.IOException; // Add IOException import
|
||||
|
||||
public class FunctionEndpoints extends AbstractEndpoint {
|
||||
|
||||
// Updated constructor to accept port
|
||||
public FunctionEndpoints(Program program, int port) {
|
||||
super(program, port); // Call super constructor
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerEndpoints(HttpServer server) {
|
||||
server.createContext("/functions", this::handleFunctions);
|
||||
server.createContext("/functions/", this::handleFunction);
|
||||
}
|
||||
|
||||
private void handleFunctions(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);
|
||||
|
||||
List<Map<String, String>> functions = new ArrayList<>();
|
||||
for (Function f : currentProgram.getFunctionManager().getFunctions(true)) {
|
||||
Map<String, String> func = new HashMap<>();
|
||||
func.put("name", f.getName());
|
||||
func.put("address", f.getEntryPoint().toString());
|
||||
functions.add(func);
|
||||
}
|
||||
|
||||
// Use sendSuccessResponse helper from AbstractEndpoint
|
||||
sendSuccessResponse(exchange, functions.subList(
|
||||
Math.max(0, offset),
|
||||
Math.min(functions.size(), offset + limit)
|
||||
));
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed"); // Uses helper from AbstractEndpoint
|
||||
}
|
||||
} catch (Exception e) {
|
||||
sendErrorResponse(exchange, 500, "Internal Server Error: " + e.getMessage()); // Uses helper from AbstractEndpoint
|
||||
}
|
||||
}
|
||||
|
||||
private void handleFunction(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
String path = exchange.getRequestURI().getPath();
|
||||
String functionName = path.substring("/functions/".length());
|
||||
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
Function function = findFunctionByName(functionName);
|
||||
if (function == null) {
|
||||
sendErrorResponse(exchange, 404, "Function not found");
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("name", function.getName());
|
||||
result.put("address", function.getEntryPoint().toString());
|
||||
result.put("signature", function.getSignature().getPrototypeString());
|
||||
|
||||
// Use sendSuccessResponse helper
|
||||
sendSuccessResponse(exchange, result);
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
sendErrorResponse(exchange, 500, "Internal Server Error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Function findFunctionByName(String name) {
|
||||
for (Function f : currentProgram.getFunctionManager().getFunctions(true)) {
|
||||
if (f.getName().equals(name)) {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// parseIntOrDefault is now inherited from AbstractEndpoint
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
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.GhydraMCPPlugin; // Need access to activeInstances
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
public class InstanceEndpoints extends AbstractEndpoint {
|
||||
|
||||
// Need a way to access the static activeInstances map from GhydraMCPPlugin
|
||||
// This is a bit awkward and suggests the instance management might need
|
||||
// a different design, perhaps a dedicated manager class.
|
||||
// For now, we pass the map or use a static accessor if made public.
|
||||
private final Map<Integer, GhydraMCPPlugin> activeInstances;
|
||||
// Note: Passing currentProgram might be null here if no program is open.
|
||||
// The constructor in AbstractEndpoint handles null program.
|
||||
|
||||
// Updated constructor to accept port
|
||||
public InstanceEndpoints(Program program, int port, Map<Integer, GhydraMCPPlugin> instances) {
|
||||
super(program, port); // Call super constructor
|
||||
this.activeInstances = instances;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerEndpoints(HttpServer server) {
|
||||
server.createContext("/instances", this::handleInstances);
|
||||
server.createContext("/registerInstance", this::handleRegisterInstance);
|
||||
server.createContext("/unregisterInstance", this::handleUnregisterInstance);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean requiresProgram() {
|
||||
// This endpoint doesn't require a program to function
|
||||
return false;
|
||||
}
|
||||
|
||||
private void handleInstances(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
List<Map<String, Object>> instanceData = new ArrayList<>();
|
||||
// Accessing the static map directly - requires it to be accessible
|
||||
// or passed in constructor.
|
||||
for (Map.Entry<Integer, GhydraMCPPlugin> entry : activeInstances.entrySet()) {
|
||||
Map<String, Object> instance = new HashMap<>();
|
||||
// Need a way to get isBaseInstance from the plugin instance - requires getter in GhydraMCPPlugin
|
||||
// instance.put("type", entry.getValue().isBaseInstance() ? "base" : "secondary"); // Placeholder access
|
||||
instance.put("type", "unknown"); // Placeholder until isBaseInstance is accessible
|
||||
instanceData.add(instance);
|
||||
}
|
||||
sendSuccessResponse(exchange, instanceData); // Use helper from AbstractEndpoint
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in /instances endpoint", e);
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); // Use helper
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRegisterInstance(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
Map<String, String> params = parseJsonPostParams(exchange);
|
||||
int regPort = parseIntOrDefault(params.get("port"), 0);
|
||||
if (regPort > 0) {
|
||||
// Logic to actually register/track the instance should happen elsewhere (e.g., main plugin or dedicated manager)
|
||||
sendSuccessResponse(exchange, Map.of("message", "Instance registration request received for port " + regPort)); // Use helper
|
||||
} else {
|
||||
sendErrorResponse(exchange, 400, "Invalid or missing port number"); // Use helper
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Msg.error(this, "Error parsing POST params for registerInstance", e);
|
||||
sendErrorResponse(exchange, 400, "Invalid request body: " + e.getMessage(), "INVALID_REQUEST"); // Use helper
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in /registerInstance", e);
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); // Use helper
|
||||
}
|
||||
}
|
||||
|
||||
private void handleUnregisterInstance(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
Map<String, String> params = parseJsonPostParams(exchange);
|
||||
int unregPort = parseIntOrDefault(params.get("port"), 0);
|
||||
if (unregPort > 0 && activeInstances.containsKey(unregPort)) {
|
||||
// Actual removal should likely happen in the main plugin's map or dedicated manager
|
||||
activeInstances.remove(unregPort); // Potential ConcurrentModificationException if map is iterated elsewhere
|
||||
sendSuccessResponse(exchange, Map.of("message", "Instance unregistered for port " + unregPort)); // Use helper
|
||||
} else {
|
||||
sendErrorResponse(exchange, 404, "No instance found on port " + unregPort, "RESOURCE_NOT_FOUND"); // Use helper
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Msg.error(this, "Error parsing POST params for unregisterInstance", e);
|
||||
sendErrorResponse(exchange, 400, "Invalid request body: " + e.getMessage(), "INVALID_REQUEST"); // Use helper
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in /unregisterInstance", e);
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage(), "INTERNAL_ERROR"); // Use helper
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Helper Methods Removed (Inherited or internal logic adjusted) ---
|
||||
|
||||
// parseIntOrDefault is inherited from AbstractEndpoint
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
package eu.starsong.ghidra.endpoints;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import ghidra.program.model.address.GlobalNamespace;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.symbol.Namespace;
|
||||
import ghidra.program.model.symbol.Symbol;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
public class NamespaceEndpoints extends AbstractEndpoint {
|
||||
|
||||
// Updated constructor to accept port
|
||||
public NamespaceEndpoints(Program program, int port) {
|
||||
super(program, port); // Call super constructor
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerEndpoints(HttpServer server) {
|
||||
server.createContext("/namespaces", this::handleNamespaces);
|
||||
}
|
||||
|
||||
private void handleNamespaces(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
Map<String, String> qparams = parseQueryParams(exchange);
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0); // Inherited
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 100); // Inherited
|
||||
Object resultData = listNamespaces(offset, limit);
|
||||
// Check if helper returned an error object
|
||||
if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) {
|
||||
sendJsonResponse(exchange, (JsonObject)resultData, 400); // Use base sendJsonResponse
|
||||
} else {
|
||||
sendSuccessResponse(exchange, resultData); // Use success helper
|
||||
}
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed"); // Inherited
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in /namespaces endpoint", e);
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); // Inherited
|
||||
}
|
||||
}
|
||||
|
||||
// --- Method moved from GhydraMCPPlugin ---
|
||||
|
||||
private JsonObject listNamespaces(int offset, int limit) {
|
||||
if (currentProgram == null) {
|
||||
return createErrorResponse("No program loaded", 400);
|
||||
}
|
||||
|
||||
Set<String> namespaces = new HashSet<>();
|
||||
for (Symbol symbol : currentProgram.getSymbolTable().getAllSymbols(true)) {
|
||||
Namespace ns = symbol.getParentNamespace();
|
||||
if (ns != null && !(ns instanceof GlobalNamespace)) {
|
||||
namespaces.add(ns.getName(true)); // Get fully qualified name
|
||||
}
|
||||
}
|
||||
|
||||
List<String> sorted = new ArrayList<>(namespaces);
|
||||
Collections.sort(sorted);
|
||||
|
||||
// Apply pagination
|
||||
int start = Math.max(0, offset);
|
||||
int end = Math.min(sorted.size(), offset + limit);
|
||||
List<String> paginated = sorted.subList(start, end);
|
||||
|
||||
return createSuccessResponse(paginated); // Keep internal helper for now
|
||||
}
|
||||
|
||||
// --- Helper Methods (Keep internal for now) ---
|
||||
|
||||
private JsonObject createSuccessResponse(Object resultData) {
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("success", true);
|
||||
response.add("result", gson.toJsonTree(resultData));
|
||||
return response;
|
||||
}
|
||||
|
||||
private JsonObject createErrorResponse(String errorMessage, int statusCode) {
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("success", false);
|
||||
response.addProperty("error", errorMessage);
|
||||
response.addProperty("status_code", statusCode);
|
||||
return response;
|
||||
}
|
||||
|
||||
// parseIntOrDefault is inherited from AbstractEndpoint
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
package eu.starsong.ghidra.endpoints;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.mem.MemoryBlock;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
public class SegmentEndpoints extends AbstractEndpoint {
|
||||
|
||||
// Updated constructor to accept port
|
||||
public SegmentEndpoints(Program program, int port) {
|
||||
super(program, port); // Call super constructor
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerEndpoints(HttpServer server) {
|
||||
server.createContext("/segments", this::handleSegments);
|
||||
}
|
||||
|
||||
private void handleSegments(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
Map<String, String> qparams = parseQueryParams(exchange);
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0); // Inherited
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 100); // Inherited
|
||||
Object resultData = listSegments(offset, limit);
|
||||
// Check if helper returned an error object
|
||||
if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) {
|
||||
sendJsonResponse(exchange, (JsonObject)resultData, 400); // Use base sendJsonResponse
|
||||
} else {
|
||||
sendSuccessResponse(exchange, resultData); // Use success helper
|
||||
}
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed"); // Inherited
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in /segments endpoint", e);
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); // Inherited
|
||||
}
|
||||
}
|
||||
|
||||
// --- Method moved from GhydraMCPPlugin ---
|
||||
|
||||
private JsonObject listSegments(int offset, int limit) {
|
||||
if (currentProgram == null) {
|
||||
return createErrorResponse("No program loaded", 400);
|
||||
}
|
||||
|
||||
List<Map<String, String>> segments = new ArrayList<>();
|
||||
for (MemoryBlock block : currentProgram.getMemory().getBlocks()) {
|
||||
Map<String, String> seg = new HashMap<>();
|
||||
seg.put("name", block.getName());
|
||||
seg.put("start", block.getStart().toString());
|
||||
seg.put("end", block.getEnd().toString());
|
||||
// Add permissions if needed: block.isRead(), block.isWrite(), block.isExecute()
|
||||
segments.add(seg);
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
int start = Math.max(0, offset);
|
||||
int end = Math.min(segments.size(), offset + limit);
|
||||
List<Map<String, String>> paginated = segments.subList(start, end);
|
||||
|
||||
return createSuccessResponse(paginated); // Keep internal helper for now
|
||||
}
|
||||
|
||||
// --- Helper Methods (Keep internal for now) ---
|
||||
|
||||
private JsonObject createSuccessResponse(Object resultData) {
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("success", true);
|
||||
response.add("result", gson.toJsonTree(resultData));
|
||||
return response;
|
||||
}
|
||||
|
||||
private JsonObject createErrorResponse(String errorMessage, int statusCode) {
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("success", false);
|
||||
response.addProperty("error", errorMessage);
|
||||
response.addProperty("status_code", statusCode);
|
||||
return response;
|
||||
}
|
||||
|
||||
// parseIntOrDefault is inherited from AbstractEndpoint
|
||||
}
|
||||
142
src/main/java/eu/starsong/ghidra/endpoints/SymbolEndpoints.java
Normal file
142
src/main/java/eu/starsong/ghidra/endpoints/SymbolEndpoints.java
Normal file
@ -0,0 +1,142 @@
|
||||
package eu.starsong.ghidra.endpoints;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.symbol.Symbol;
|
||||
import ghidra.program.model.symbol.SymbolIterator;
|
||||
import ghidra.program.model.symbol.SymbolTable;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
public class SymbolEndpoints extends AbstractEndpoint {
|
||||
|
||||
// Updated constructor to accept port
|
||||
public SymbolEndpoints(Program program, int port) {
|
||||
super(program, port); // Call super constructor
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerEndpoints(HttpServer server) {
|
||||
server.createContext("/symbols/imports", this::handleImports);
|
||||
server.createContext("/symbols/exports", this::handleExports);
|
||||
}
|
||||
|
||||
private void handleImports(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
Map<String, String> qparams = parseQueryParams(exchange);
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0); // Inherited
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 100); // Inherited
|
||||
Object resultData = listImports(offset, limit);
|
||||
// Check if helper returned an error object
|
||||
if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) {
|
||||
sendJsonResponse(exchange, (JsonObject)resultData, 400); // Use base sendJsonResponse
|
||||
} else {
|
||||
sendSuccessResponse(exchange, resultData); // Use success helper
|
||||
}
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed"); // Inherited
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in /symbols/imports endpoint", e);
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); // Inherited
|
||||
}
|
||||
}
|
||||
|
||||
private void handleExports(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
Map<String, String> qparams = parseQueryParams(exchange);
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0); // Inherited
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 100); // Inherited
|
||||
Object resultData = listExports(offset, limit);
|
||||
// Check if helper returned an error object
|
||||
if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) {
|
||||
sendJsonResponse(exchange, (JsonObject)resultData, 400); // Use base sendJsonResponse
|
||||
} else {
|
||||
sendSuccessResponse(exchange, resultData); // Use success helper
|
||||
}
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed"); // Inherited
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in /symbols/exports endpoint", e);
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); // Inherited
|
||||
}
|
||||
}
|
||||
|
||||
// --- Methods moved from GhydraMCPPlugin ---
|
||||
|
||||
private JsonObject listImports(int offset, int limit) {
|
||||
if (currentProgram == null) {
|
||||
return createErrorResponse("No program loaded", 400);
|
||||
}
|
||||
|
||||
List<Map<String, String>> imports = new ArrayList<>();
|
||||
for (Symbol symbol : currentProgram.getSymbolTable().getExternalSymbols()) {
|
||||
Map<String, String> imp = new HashMap<>();
|
||||
imp.put("name", symbol.getName());
|
||||
imp.put("address", symbol.getAddress().toString());
|
||||
// Add library name if needed: symbol.getLibraryName()
|
||||
imports.add(imp);
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
int start = Math.max(0, offset);
|
||||
int end = Math.min(imports.size(), offset + limit);
|
||||
List<Map<String, String>> paginated = imports.subList(start, end);
|
||||
|
||||
return createSuccessResponse(paginated);
|
||||
}
|
||||
|
||||
private JsonObject listExports(int offset, int limit) {
|
||||
if (currentProgram == null) {
|
||||
return createErrorResponse("No program loaded", 400);
|
||||
}
|
||||
|
||||
List<Map<String, String>> exports = new ArrayList<>();
|
||||
SymbolTable table = currentProgram.getSymbolTable();
|
||||
SymbolIterator it = table.getAllSymbols(true);
|
||||
|
||||
while (it.hasNext()) {
|
||||
Symbol s = it.next();
|
||||
if (s.isExternalEntryPoint()) {
|
||||
Map<String, String> exp = new HashMap<>();
|
||||
exp.put("name", s.getName());
|
||||
exp.put("address", s.getAddress().toString());
|
||||
exports.add(exp);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
int start = Math.max(0, offset);
|
||||
int end = Math.min(exports.size(), offset + limit);
|
||||
List<Map<String, String>> paginated = exports.subList(start, end);
|
||||
|
||||
return createSuccessResponse(paginated); // Keep internal helper for now
|
||||
}
|
||||
|
||||
// --- Helper Methods (Keep internal for now, refactor later if needed) ---
|
||||
// Note: These might differ slightly from AbstractEndpoint/ResponseBuilder, review needed.
|
||||
|
||||
private JsonObject createSuccessResponse(Object resultData) {
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("success", true);
|
||||
response.add("result", gson.toJsonTree(resultData));
|
||||
return response;
|
||||
}
|
||||
|
||||
private JsonObject createErrorResponse(String errorMessage, int statusCode) {
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("success", false);
|
||||
response.addProperty("error", errorMessage);
|
||||
response.addProperty("status_code", statusCode);
|
||||
return response;
|
||||
}
|
||||
|
||||
// parseIntOrDefault is inherited from AbstractEndpoint
|
||||
}
|
||||
@ -0,0 +1,265 @@
|
||||
package eu.starsong.ghidra.endpoints;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import eu.starsong.ghidra.util.TransactionHelper;
|
||||
import eu.starsong.ghidra.util.TransactionHelper.TransactionException;
|
||||
import ghidra.app.decompiler.DecompInterface;
|
||||
import ghidra.app.decompiler.DecompileResults;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.data.DataType;
|
||||
import ghidra.program.model.listing.Function;
|
||||
import ghidra.program.model.listing.Parameter;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.listing.VariableStorage;
|
||||
import ghidra.program.model.pcode.HighFunction;
|
||||
import ghidra.program.model.pcode.HighFunctionDBUtil;
|
||||
import ghidra.program.model.pcode.HighSymbol;
|
||||
import ghidra.program.model.pcode.LocalSymbolMap;
|
||||
import ghidra.program.model.symbol.SourceType;
|
||||
import ghidra.program.model.symbol.Symbol;
|
||||
import ghidra.program.model.symbol.SymbolIterator;
|
||||
import ghidra.program.model.symbol.SymbolTable;
|
||||
import ghidra.program.model.symbol.SymbolType;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.task.ConsoleTaskMonitor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
|
||||
public class VariableEndpoints extends AbstractEndpoint {
|
||||
|
||||
// Updated constructor to accept port
|
||||
public VariableEndpoints(Program program, int port) {
|
||||
super(program, port); // Call super constructor
|
||||
}
|
||||
|
||||
@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.
|
||||
}
|
||||
|
||||
private void handleGlobalVariables(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
Map<String, String> qparams = parseQueryParams(exchange);
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||
String search = qparams.get("search"); // Renamed from 'query' for clarity
|
||||
|
||||
Object resultData;
|
||||
if (search != null && !search.isEmpty()) {
|
||||
resultData = searchVariables(search, offset, limit);
|
||||
} else {
|
||||
resultData = listVariables(offset, limit);
|
||||
}
|
||||
// Check if helper returned an error object
|
||||
if (resultData instanceof JsonObject && !((JsonObject)resultData).get("success").getAsBoolean()) {
|
||||
sendJsonResponse(exchange, (JsonObject)resultData, 400); // Use base sendJsonResponse
|
||||
} else {
|
||||
sendSuccessResponse(exchange, resultData); // Use success helper
|
||||
}
|
||||
} else {
|
||||
sendErrorResponse(exchange, 405, "Method Not Allowed");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error in /variables endpoint", e);
|
||||
sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// --- Methods moved from GhydraMCPPlugin ---
|
||||
|
||||
private JsonObject listVariables(int offset, int limit) {
|
||||
if (currentProgram == null) {
|
||||
return createErrorResponse("No program loaded", 400);
|
||||
}
|
||||
|
||||
List<Map<String, String>> variables = new ArrayList<>();
|
||||
|
||||
// Get global variables
|
||||
SymbolTable symbolTable = currentProgram.getSymbolTable();
|
||||
for (Symbol symbol : symbolTable.getDefinedSymbols()) {
|
||||
if (symbol.isGlobal() && !symbol.isExternal() &&
|
||||
symbol.getSymbolType() != SymbolType.FUNCTION &&
|
||||
symbol.getSymbolType() != SymbolType.LABEL) {
|
||||
|
||||
Map<String, String> varInfo = new HashMap<>();
|
||||
varInfo.put("name", symbol.getName());
|
||||
varInfo.put("address", symbol.getAddress().toString());
|
||||
varInfo.put("type", "global");
|
||||
varInfo.put("dataType", getDataTypeName(currentProgram, symbol.getAddress()));
|
||||
variables.add(varInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Get local variables from all functions (Consider performance implications)
|
||||
DecompInterface decomp = null;
|
||||
try {
|
||||
decomp = new DecompInterface();
|
||||
if (!decomp.openProgram(currentProgram)) {
|
||||
Msg.error(this, "listVariables: Failed to open program with decompiler.");
|
||||
} else {
|
||||
for (Function function : currentProgram.getFunctionManager().getFunctions(true)) {
|
||||
try {
|
||||
DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor());
|
||||
if (results != null && results.decompileCompleted()) {
|
||||
HighFunction highFunc = results.getHighFunction();
|
||||
if (highFunc != null) {
|
||||
Iterator<HighSymbol> symbolIter = highFunc.getLocalSymbolMap().getSymbols();
|
||||
while (symbolIter.hasNext()) {
|
||||
HighSymbol symbol = symbolIter.next();
|
||||
if (!symbol.isParameter()) { // Only list locals
|
||||
Map<String, String> varInfo = new HashMap<>();
|
||||
varInfo.put("name", symbol.getName());
|
||||
varInfo.put("type", "local");
|
||||
varInfo.put("function", function.getName());
|
||||
Address pcAddr = symbol.getPCAddress();
|
||||
varInfo.put("address", pcAddr != null ? pcAddr.toString() : "N/A");
|
||||
varInfo.put("dataType", symbol.getDataType() != null ? symbol.getDataType().getName() : "unknown");
|
||||
variables.add(varInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "listVariables: Error processing function " + function.getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "listVariables: Error during local variable processing", e);
|
||||
} finally {
|
||||
if (decomp != null) {
|
||||
decomp.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(variables, Comparator.comparing(a -> a.get("name")));
|
||||
|
||||
int start = Math.max(0, offset);
|
||||
int end = Math.min(variables.size(), offset + limit);
|
||||
List<Map<String, String>> paginated = variables.subList(start, end);
|
||||
|
||||
return createSuccessResponse(paginated); // Keep using internal helper for now
|
||||
}
|
||||
|
||||
private JsonObject searchVariables(String searchTerm, int offset, int limit) {
|
||||
if (currentProgram == null) {
|
||||
return createErrorResponse("No program loaded", 400); // Keep using internal helper
|
||||
}
|
||||
if (searchTerm == null || searchTerm.isEmpty()) {
|
||||
return createErrorResponse("Search term is required", 400); // Keep using internal helper
|
||||
}
|
||||
|
||||
List<Map<String, String>> matchedVars = new ArrayList<>();
|
||||
String lowerSearchTerm = searchTerm.toLowerCase();
|
||||
|
||||
// Search global variables
|
||||
SymbolTable symbolTable = currentProgram.getSymbolTable();
|
||||
SymbolIterator it = symbolTable.getSymbolIterator();
|
||||
while (it.hasNext()) {
|
||||
Symbol symbol = it.next();
|
||||
if (symbol.isGlobal() &&
|
||||
symbol.getSymbolType() != SymbolType.FUNCTION &&
|
||||
symbol.getSymbolType() != SymbolType.LABEL &&
|
||||
symbol.getName().toLowerCase().contains(lowerSearchTerm)) {
|
||||
Map<String, String> varInfo = new HashMap<>();
|
||||
varInfo.put("name", symbol.getName());
|
||||
varInfo.put("address", symbol.getAddress().toString());
|
||||
varInfo.put("type", "global");
|
||||
varInfo.put("dataType", getDataTypeName(currentProgram, symbol.getAddress()));
|
||||
matchedVars.add(varInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Search local variables
|
||||
DecompInterface decomp = null;
|
||||
try {
|
||||
decomp = new DecompInterface();
|
||||
if (decomp.openProgram(currentProgram)) {
|
||||
for (Function function : currentProgram.getFunctionManager().getFunctions(true)) {
|
||||
try {
|
||||
DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor());
|
||||
if (results != null && results.decompileCompleted()) {
|
||||
HighFunction highFunc = results.getHighFunction();
|
||||
if (highFunc != null) {
|
||||
Iterator<HighSymbol> symbolIter = highFunc.getLocalSymbolMap().getSymbols();
|
||||
while (symbolIter.hasNext()) {
|
||||
HighSymbol symbol = symbolIter.next();
|
||||
if (symbol.getName().toLowerCase().contains(lowerSearchTerm)) {
|
||||
Map<String, String> varInfo = new HashMap<>();
|
||||
varInfo.put("name", symbol.getName());
|
||||
varInfo.put("function", function.getName());
|
||||
varInfo.put("type", symbol.isParameter() ? "parameter" : "local");
|
||||
Address pcAddr = symbol.getPCAddress();
|
||||
varInfo.put("address", pcAddr != null ? pcAddr.toString() : "N/A");
|
||||
varInfo.put("dataType", symbol.getDataType() != null ? symbol.getDataType().getName() : "unknown");
|
||||
matchedVars.add(varInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.warn(this, "searchVariables: Error processing function " + function.getName(), e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Msg.error(this, "searchVariables: Failed to open program with decompiler.");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "searchVariables: Error during local variable search", e);
|
||||
} finally {
|
||||
if (decomp != null) {
|
||||
decomp.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(matchedVars, Comparator.comparing(a -> a.get("name")));
|
||||
|
||||
int start = Math.max(0, offset);
|
||||
int end = Math.min(matchedVars.size(), offset + limit);
|
||||
List<Map<String, String>> paginated = matchedVars.subList(start, end);
|
||||
|
||||
return createSuccessResponse(paginated); // Keep using internal helper
|
||||
}
|
||||
|
||||
// --- Helper Methods (Keep internal for now, refactor later if needed) ---
|
||||
|
||||
private String getDataTypeName(Program program, Address address) {
|
||||
// This might be better in GhidraUtil if used elsewhere
|
||||
ghidra.program.model.listing.Data data = program.getListing().getDataAt(address);
|
||||
if (data == null) return "undefined";
|
||||
DataType dt = data.getDataType();
|
||||
return dt != null ? dt.getName() : "unknown";
|
||||
}
|
||||
|
||||
// Keep internal response helpers for now, as they differ slightly from AbstractEndpoint's
|
||||
private JsonObject createSuccessResponse(Object resultData) {
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("success", true);
|
||||
response.add("result", gson.toJsonTree(resultData));
|
||||
// These helpers don't add id/instance/_links, unlike ResponseBuilder
|
||||
return response;
|
||||
}
|
||||
|
||||
private JsonObject createErrorResponse(String errorMessage, int statusCode) {
|
||||
JsonObject response = new JsonObject();
|
||||
response.addProperty("success", false);
|
||||
response.addProperty("error", errorMessage);
|
||||
response.addProperty("status_code", statusCode);
|
||||
return response;
|
||||
}
|
||||
|
||||
// parseIntOrDefault is inherited from AbstractEndpoint
|
||||
}
|
||||
395
src/main/java/eu/starsong/ghidra/model/FunctionInfo.java
Normal file
395
src/main/java/eu/starsong/ghidra/model/FunctionInfo.java
Normal file
@ -0,0 +1,395 @@
|
||||
package eu.starsong.ghidra.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Model class representing Ghidra function information.
|
||||
* This provides a structured object for function data instead of using Map<String, Object>.
|
||||
*/
|
||||
public class FunctionInfo {
|
||||
private String name;
|
||||
private String address;
|
||||
private String signature;
|
||||
private String returnType;
|
||||
private List<ParameterInfo> parameters;
|
||||
private String decompilation;
|
||||
private boolean isExternal;
|
||||
private String callingConvention;
|
||||
private String namespace;
|
||||
|
||||
/**
|
||||
* Default constructor for serialization frameworks
|
||||
*/
|
||||
public FunctionInfo() {
|
||||
this.parameters = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with essential fields
|
||||
*/
|
||||
public FunctionInfo(String name, String address, String signature) {
|
||||
this.name = name;
|
||||
this.address = address;
|
||||
this.signature = signature;
|
||||
this.parameters = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Full constructor
|
||||
*/
|
||||
public FunctionInfo(String name, String address, String signature, String returnType,
|
||||
List<ParameterInfo> parameters, String decompilation,
|
||||
boolean isExternal, String callingConvention, String namespace) {
|
||||
this.name = name;
|
||||
this.address = address;
|
||||
this.signature = signature;
|
||||
this.returnType = returnType;
|
||||
this.parameters = parameters != null ? parameters : new ArrayList<>();
|
||||
this.decompilation = decompilation;
|
||||
this.isExternal = isExternal;
|
||||
this.callingConvention = callingConvention;
|
||||
this.namespace = namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The function name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name The function name
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The function entry point address
|
||||
*/
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param address The function entry point address
|
||||
*/
|
||||
public void setAddress(String address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The function signature (prototype string)
|
||||
*/
|
||||
public String getSignature() {
|
||||
return signature;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param signature The function signature
|
||||
*/
|
||||
public void setSignature(String signature) {
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The function return type
|
||||
*/
|
||||
public String getReturnType() {
|
||||
return returnType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param returnType The function return type
|
||||
*/
|
||||
public void setReturnType(String returnType) {
|
||||
this.returnType = returnType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The function parameters
|
||||
*/
|
||||
public List<ParameterInfo> getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param parameters The function parameters
|
||||
*/
|
||||
public void setParameters(List<ParameterInfo> parameters) {
|
||||
this.parameters = parameters != null ? parameters : new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The decompiled C code for the function
|
||||
*/
|
||||
public String getDecompilation() {
|
||||
return decompilation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param decompilation The decompiled C code
|
||||
*/
|
||||
public void setDecompilation(String decompilation) {
|
||||
this.decompilation = decompilation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether the function is external (imported)
|
||||
*/
|
||||
public boolean isExternal() {
|
||||
return isExternal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param external Whether the function is external
|
||||
*/
|
||||
public void setExternal(boolean external) {
|
||||
isExternal = external;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The function's calling convention
|
||||
*/
|
||||
public String getCallingConvention() {
|
||||
return callingConvention;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callingConvention The function's calling convention
|
||||
*/
|
||||
public void setCallingConvention(String callingConvention) {
|
||||
this.callingConvention = callingConvention;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The function's namespace
|
||||
*/
|
||||
public String getNamespace() {
|
||||
return namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param namespace The function's namespace
|
||||
*/
|
||||
public void setNamespace(String namespace) {
|
||||
this.namespace = namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a parameter to the function
|
||||
* @param parameter The parameter to add
|
||||
*/
|
||||
public void addParameter(ParameterInfo parameter) {
|
||||
if (parameter != null) {
|
||||
this.parameters.add(parameter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder pattern for FunctionInfo
|
||||
*/
|
||||
public static class Builder {
|
||||
private String name;
|
||||
private String address;
|
||||
private String signature;
|
||||
private String returnType;
|
||||
private List<ParameterInfo> parameters = new ArrayList<>();
|
||||
private String decompilation;
|
||||
private boolean isExternal;
|
||||
private String callingConvention;
|
||||
private String namespace;
|
||||
|
||||
public Builder name(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder address(String address) {
|
||||
this.address = address;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder signature(String signature) {
|
||||
this.signature = signature;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder returnType(String returnType) {
|
||||
this.returnType = returnType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder parameters(List<ParameterInfo> parameters) {
|
||||
this.parameters = parameters;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addParameter(ParameterInfo parameter) {
|
||||
this.parameters.add(parameter);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder decompilation(String decompilation) {
|
||||
this.decompilation = decompilation;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder isExternal(boolean isExternal) {
|
||||
this.isExternal = isExternal;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder callingConvention(String callingConvention) {
|
||||
this.callingConvention = callingConvention;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder namespace(String namespace) {
|
||||
this.namespace = namespace;
|
||||
return this;
|
||||
}
|
||||
|
||||
public FunctionInfo build() {
|
||||
return new FunctionInfo(
|
||||
name, address, signature, returnType,
|
||||
parameters, decompilation, isExternal,
|
||||
callingConvention, namespace
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new builder for FunctionInfo
|
||||
* @return A new builder instance
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner class representing function parameter information
|
||||
*/
|
||||
public static class ParameterInfo {
|
||||
private String name;
|
||||
private String dataType;
|
||||
private int ordinal;
|
||||
private String storage;
|
||||
|
||||
/**
|
||||
* Default constructor for serialization frameworks
|
||||
*/
|
||||
public ParameterInfo() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Full constructor
|
||||
*/
|
||||
public ParameterInfo(String name, String dataType, int ordinal, String storage) {
|
||||
this.name = name;
|
||||
this.dataType = dataType;
|
||||
this.ordinal = ordinal;
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The parameter name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name The parameter name
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The parameter data type
|
||||
*/
|
||||
public String getDataType() {
|
||||
return dataType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dataType The parameter data type
|
||||
*/
|
||||
public void setDataType(String dataType) {
|
||||
this.dataType = dataType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The parameter position (0-based)
|
||||
*/
|
||||
public int getOrdinal() {
|
||||
return ordinal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ordinal The parameter position
|
||||
*/
|
||||
public void setOrdinal(int ordinal) {
|
||||
this.ordinal = ordinal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The parameter storage location
|
||||
*/
|
||||
public String getStorage() {
|
||||
return storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param storage The parameter storage location
|
||||
*/
|
||||
public void setStorage(String storage) {
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder pattern for ParameterInfo
|
||||
*/
|
||||
public static class Builder {
|
||||
private String name;
|
||||
private String dataType;
|
||||
private int ordinal;
|
||||
private String storage;
|
||||
|
||||
public Builder name(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder dataType(String dataType) {
|
||||
this.dataType = dataType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder ordinal(int ordinal) {
|
||||
this.ordinal = ordinal;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder storage(String storage) {
|
||||
this.storage = storage;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ParameterInfo build() {
|
||||
return new ParameterInfo(name, dataType, ordinal, storage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new builder for ParameterInfo
|
||||
* @return A new builder instance
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
}
|
||||
}
|
||||
175
src/main/java/eu/starsong/ghidra/model/JsonResponse.java
Normal file
175
src/main/java/eu/starsong/ghidra/model/JsonResponse.java
Normal file
@ -0,0 +1,175 @@
|
||||
package eu.starsong.ghidra.model;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Standardized response object for API responses.
|
||||
* This class follows the common response structure used throughout the API.
|
||||
*/
|
||||
public class JsonResponse {
|
||||
private boolean success;
|
||||
private Object result;
|
||||
private Map<String, Object> error;
|
||||
private Map<String, Object> links;
|
||||
private String id;
|
||||
private String instance;
|
||||
|
||||
// Private constructor for builder pattern
|
||||
private JsonResponse() {
|
||||
this.links = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether the request was successful
|
||||
*/
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The result data for successful requests
|
||||
*/
|
||||
public Object getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Error information for failed requests
|
||||
*/
|
||||
public Map<String, Object> getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HATEOAS links
|
||||
*/
|
||||
public Map<String, Object> getLinks() {
|
||||
return links;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Request ID
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Server instance information
|
||||
*/
|
||||
public String getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new builder for constructing a JsonResponse
|
||||
* @return A new builder instance
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class for JsonResponse
|
||||
*/
|
||||
public static class Builder {
|
||||
private final JsonResponse response;
|
||||
|
||||
private Builder() {
|
||||
response = new JsonResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the success status
|
||||
* @param success Whether the request was successful
|
||||
* @return This builder
|
||||
*/
|
||||
public Builder success(boolean success) {
|
||||
response.success = success;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the result data
|
||||
* @param result The result data
|
||||
* @return This builder
|
||||
*/
|
||||
public Builder result(Object result) {
|
||||
response.result = result;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set error information
|
||||
* @param message Error message
|
||||
* @param code Error code
|
||||
* @return This builder
|
||||
*/
|
||||
public Builder error(String message, String code) {
|
||||
Map<String, Object> error = new HashMap<>();
|
||||
error.put("message", message);
|
||||
if (code != null && !code.isEmpty()) {
|
||||
error.put("code", code);
|
||||
}
|
||||
response.error = error;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a link
|
||||
* @param rel Relation name
|
||||
* @param href Link URL
|
||||
* @return This builder
|
||||
*/
|
||||
public Builder addLink(String rel, String href) {
|
||||
Map<String, String> link = new HashMap<>();
|
||||
link.put("href", href);
|
||||
response.links.put(rel, link);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a link with method
|
||||
* @param rel Relation name
|
||||
* @param href Link URL
|
||||
* @param method HTTP method
|
||||
* @return This builder
|
||||
*/
|
||||
public Builder addLink(String rel, String href, String method) {
|
||||
Map<String, String> link = new HashMap<>();
|
||||
link.put("href", href);
|
||||
link.put("method", method);
|
||||
response.links.put(rel, link);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set request ID
|
||||
* @param id Request ID
|
||||
* @return This builder
|
||||
*/
|
||||
public Builder id(String id) {
|
||||
response.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set instance information
|
||||
* @param instance Instance information
|
||||
* @return This builder
|
||||
*/
|
||||
public Builder instance(String instance) {
|
||||
response.instance = instance;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the JsonResponse
|
||||
* @return The constructed JsonResponse
|
||||
*/
|
||||
public JsonResponse build() {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
218
src/main/java/eu/starsong/ghidra/model/ProgramInfo.java
Normal file
218
src/main/java/eu/starsong/ghidra/model/ProgramInfo.java
Normal file
@ -0,0 +1,218 @@
|
||||
package eu.starsong.ghidra.model;
|
||||
|
||||
/**
|
||||
* Model class representing Ghidra program information.
|
||||
* This provides a structured object for program data instead of using Map<String, Object>.
|
||||
*/
|
||||
public class ProgramInfo {
|
||||
private String programId;
|
||||
private String name;
|
||||
private String languageId;
|
||||
private String compilerSpecId;
|
||||
private String imageBase;
|
||||
private long memorySize;
|
||||
private boolean isOpen;
|
||||
private boolean analysisComplete;
|
||||
|
||||
/**
|
||||
* Default constructor for serialization frameworks
|
||||
*/
|
||||
public ProgramInfo() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Full constructor
|
||||
*/
|
||||
public ProgramInfo(String programId, String name, String languageId, String compilerSpecId,
|
||||
String imageBase, long memorySize, boolean isOpen, boolean analysisComplete) {
|
||||
this.programId = programId;
|
||||
this.name = name;
|
||||
this.languageId = languageId;
|
||||
this.compilerSpecId = compilerSpecId;
|
||||
this.imageBase = imageBase;
|
||||
this.memorySize = memorySize;
|
||||
this.isOpen = isOpen;
|
||||
this.analysisComplete = analysisComplete;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The program's unique identifier (typically the file pathname)
|
||||
*/
|
||||
public String getProgramId() {
|
||||
return programId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param programId The program's unique identifier
|
||||
*/
|
||||
public void setProgramId(String programId) {
|
||||
this.programId = programId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The program's name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name The program's name
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The program's language ID
|
||||
*/
|
||||
public String getLanguageId() {
|
||||
return languageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param languageId The program's language ID
|
||||
*/
|
||||
public void setLanguageId(String languageId) {
|
||||
this.languageId = languageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The program's compiler specification ID
|
||||
*/
|
||||
public String getCompilerSpecId() {
|
||||
return compilerSpecId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param compilerSpecId The program's compiler specification ID
|
||||
*/
|
||||
public void setCompilerSpecId(String compilerSpecId) {
|
||||
this.compilerSpecId = compilerSpecId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The program's image base address
|
||||
*/
|
||||
public String getImageBase() {
|
||||
return imageBase;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param imageBase The program's image base address
|
||||
*/
|
||||
public void setImageBase(String imageBase) {
|
||||
this.imageBase = imageBase;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The program's memory size in bytes
|
||||
*/
|
||||
public long getMemorySize() {
|
||||
return memorySize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param memorySize The program's memory size in bytes
|
||||
*/
|
||||
public void setMemorySize(long memorySize) {
|
||||
this.memorySize = memorySize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether the program is currently open
|
||||
*/
|
||||
public boolean isOpen() {
|
||||
return isOpen;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param open Whether the program is currently open
|
||||
*/
|
||||
public void setOpen(boolean open) {
|
||||
isOpen = open;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether analysis has been completed on the program
|
||||
*/
|
||||
public boolean isAnalysisComplete() {
|
||||
return analysisComplete;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param analysisComplete Whether analysis has been completed on the program
|
||||
*/
|
||||
public void setAnalysisComplete(boolean analysisComplete) {
|
||||
this.analysisComplete = analysisComplete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder pattern for ProgramInfo
|
||||
*/
|
||||
public static class Builder {
|
||||
private String programId;
|
||||
private String name;
|
||||
private String languageId;
|
||||
private String compilerSpecId;
|
||||
private String imageBase;
|
||||
private long memorySize;
|
||||
private boolean isOpen;
|
||||
private boolean analysisComplete;
|
||||
|
||||
public Builder programId(String programId) {
|
||||
this.programId = programId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder name(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder languageId(String languageId) {
|
||||
this.languageId = languageId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder compilerSpecId(String compilerSpecId) {
|
||||
this.compilerSpecId = compilerSpecId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder imageBase(String imageBase) {
|
||||
this.imageBase = imageBase;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder memorySize(long memorySize) {
|
||||
this.memorySize = memorySize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder isOpen(boolean isOpen) {
|
||||
this.isOpen = isOpen;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder analysisComplete(boolean analysisComplete) {
|
||||
this.analysisComplete = analysisComplete;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProgramInfo build() {
|
||||
return new ProgramInfo(
|
||||
programId, name, languageId, compilerSpecId,
|
||||
imageBase, memorySize, isOpen, analysisComplete
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new builder for ProgramInfo
|
||||
* @return A new builder instance
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
}
|
||||
226
src/main/java/eu/starsong/ghidra/model/VariableInfo.java
Normal file
226
src/main/java/eu/starsong/ghidra/model/VariableInfo.java
Normal file
@ -0,0 +1,226 @@
|
||||
package eu.starsong.ghidra.model;
|
||||
|
||||
/**
|
||||
* Model class representing Ghidra variable information.
|
||||
* This provides a structured object for variable data instead of using Map<String, Object>.
|
||||
*/
|
||||
public class VariableInfo {
|
||||
private String name;
|
||||
private String dataType;
|
||||
private String address;
|
||||
private String type; // "local", "parameter", "global", etc.
|
||||
private String function; // Function name if local/parameter
|
||||
private String storage; // Storage location
|
||||
private String value; // Value if known
|
||||
|
||||
/**
|
||||
* Default constructor for serialization frameworks
|
||||
*/
|
||||
public VariableInfo() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with essential fields
|
||||
*/
|
||||
public VariableInfo(String name, String dataType, String type) {
|
||||
this.name = name;
|
||||
this.dataType = dataType;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full constructor
|
||||
*/
|
||||
public VariableInfo(String name, String dataType, String address, String type,
|
||||
String function, String storage, String value) {
|
||||
this.name = name;
|
||||
this.dataType = dataType;
|
||||
this.address = address;
|
||||
this.type = type;
|
||||
this.function = function;
|
||||
this.storage = storage;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The variable name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name The variable name
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The variable data type
|
||||
*/
|
||||
public String getDataType() {
|
||||
return dataType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dataType The variable data type
|
||||
*/
|
||||
public void setDataType(String dataType) {
|
||||
this.dataType = dataType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The variable address (if applicable)
|
||||
*/
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param address The variable address
|
||||
*/
|
||||
public void setAddress(String address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The variable type (local, parameter, global, etc.)
|
||||
*/
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param type The variable type
|
||||
*/
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The function name (for local variables and parameters)
|
||||
*/
|
||||
public String getFunction() {
|
||||
return function;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param function The function name
|
||||
*/
|
||||
public void setFunction(String function) {
|
||||
this.function = function;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The variable storage location
|
||||
*/
|
||||
public String getStorage() {
|
||||
return storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param storage The variable storage location
|
||||
*/
|
||||
public void setStorage(String storage) {
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The variable value (if known)
|
||||
*/
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value The variable value
|
||||
*/
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether this variable is a local variable
|
||||
*/
|
||||
public boolean isLocal() {
|
||||
return "local".equals(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether this variable is a parameter
|
||||
*/
|
||||
public boolean isParameter() {
|
||||
return "parameter".equals(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether this variable is a global variable
|
||||
*/
|
||||
public boolean isGlobal() {
|
||||
return "global".equals(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder pattern for VariableInfo
|
||||
*/
|
||||
public static class Builder {
|
||||
private String name;
|
||||
private String dataType;
|
||||
private String address;
|
||||
private String type;
|
||||
private String function;
|
||||
private String storage;
|
||||
private String value;
|
||||
|
||||
public Builder name(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder dataType(String dataType) {
|
||||
this.dataType = dataType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder address(String address) {
|
||||
this.address = address;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder type(String type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder function(String function) {
|
||||
this.function = function;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder storage(String storage) {
|
||||
this.storage = storage;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder value(String value) {
|
||||
this.value = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public VariableInfo build() {
|
||||
return new VariableInfo(
|
||||
name, dataType, address, type,
|
||||
function, storage, value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new builder for VariableInfo
|
||||
* @return A new builder instance
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package eu.starsong.ghidra.util;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface GhidraSupplier<T> {
|
||||
T get() throws Exception;
|
||||
}
|
||||
287
src/main/java/eu/starsong/ghidra/util/GhidraUtil.java
Normal file
287
src/main/java/eu/starsong/ghidra/util/GhidraUtil.java
Normal file
@ -0,0 +1,287 @@
|
||||
package eu.starsong.ghidra.util;
|
||||
|
||||
import ghidra.app.decompiler.DecompInterface;
|
||||
import ghidra.app.decompiler.DecompileOptions;
|
||||
import ghidra.app.decompiler.DecompileResults;
|
||||
import ghidra.app.services.GoToService;
|
||||
import ghidra.app.services.ProgramManager;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.address.AddressFactory;
|
||||
import ghidra.program.model.data.DataType;
|
||||
import ghidra.program.model.data.DataTypeManager;
|
||||
import ghidra.program.model.listing.Function;
|
||||
import ghidra.program.model.listing.FunctionManager;
|
||||
import ghidra.program.model.listing.Parameter;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.model.listing.Variable;
|
||||
import ghidra.program.model.pcode.HighFunction;
|
||||
import ghidra.program.model.pcode.HighVariable;
|
||||
import ghidra.program.model.pcode.PcodeOp;
|
||||
import ghidra.program.model.pcode.Varnode;
|
||||
import ghidra.program.model.symbol.SymbolTable;
|
||||
import ghidra.program.util.ProgramLocation;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class GhidraUtil {
|
||||
|
||||
/**
|
||||
* Parse an integer from a string, or return defaultValue if null/invalid.
|
||||
*/
|
||||
public static int parseIntOrDefault(String val, int defaultValue) {
|
||||
if (val == null) return defaultValue;
|
||||
try {
|
||||
return Integer.parseInt(val);
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a data type by name within the program's data type managers.
|
||||
* @param program The current program.
|
||||
* @param dataTypeName The name of the data type to find.
|
||||
* @return The found DataType, or null if not found.
|
||||
*/
|
||||
public static DataType findDataType(Program program, String dataTypeName) {
|
||||
if (program == null || dataTypeName == null || dataTypeName.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
DataTypeManager dtm = program.getDataTypeManager();
|
||||
List<DataType> foundTypes = new ArrayList<>();
|
||||
dtm.findDataTypes(dataTypeName, foundTypes);
|
||||
|
||||
if (!foundTypes.isEmpty()) {
|
||||
// Prefer the first match, might need more sophisticated logic
|
||||
// if multiple types with the same name exist in different categories.
|
||||
return foundTypes.get(0);
|
||||
} else {
|
||||
Msg.warn(GhidraUtil.class, "Data type not found: " + dataTypeName);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current address as a string from the Ghidra tool.
|
||||
* @param tool The Ghidra plugin tool.
|
||||
* @return The current address as a string, or null if not available.
|
||||
*/
|
||||
public static String getCurrentAddressString(PluginTool tool) {
|
||||
if (tool == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get current program
|
||||
Program program = tool.getService(ProgramManager.class).getCurrentProgram();
|
||||
if (program == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return the current address
|
||||
return "00000000"; // Placeholder - actual implementation would get current cursor position
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information about the current function in the Ghidra tool.
|
||||
* @param tool The Ghidra plugin tool.
|
||||
* @param program The current program.
|
||||
* @return A map containing information about the current function, or an empty map if not available.
|
||||
*/
|
||||
public static Map<String, Object> getCurrentFunctionInfo(PluginTool tool, Program program) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
if (tool == null || program == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// For now, just return the first function in the program as a placeholder
|
||||
FunctionManager functionManager = program.getFunctionManager();
|
||||
Function function = null;
|
||||
|
||||
for (Function f : functionManager.getFunctions(true)) {
|
||||
function = f;
|
||||
break;
|
||||
}
|
||||
|
||||
if (function == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result.put("name", function.getName());
|
||||
result.put("address", function.getEntryPoint().toString());
|
||||
result.put("signature", function.getSignature().getPrototypeString());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information about a function at the specified address.
|
||||
* @param program The current program.
|
||||
* @param addressStr The address as a string.
|
||||
* @return A map containing information about the function, or an empty map if not found.
|
||||
*/
|
||||
public static Map<String, Object> getFunctionByAddress(Program program, String addressStr) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
if (program == null || addressStr == null || addressStr.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
AddressFactory addressFactory = program.getAddressFactory();
|
||||
Address address;
|
||||
|
||||
try {
|
||||
address = addressFactory.getAddress(addressStr);
|
||||
} catch (Exception e) {
|
||||
Msg.error(GhidraUtil.class, "Invalid address format: " + addressStr, e);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (address == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
FunctionManager functionManager = program.getFunctionManager();
|
||||
Function function = functionManager.getFunctionAt(address);
|
||||
|
||||
if (function == null) {
|
||||
function = functionManager.getFunctionContaining(address);
|
||||
}
|
||||
|
||||
if (function == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result.put("name", function.getName());
|
||||
result.put("address", function.getEntryPoint().toString());
|
||||
result.put("signature", function.getSignature().getPrototypeString());
|
||||
|
||||
// Add decompilation
|
||||
String decompilation = decompileFunction(function);
|
||||
result.put("decompilation", decompilation != null ? decompilation : "");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompiles a function at the specified address.
|
||||
* @param program The current program.
|
||||
* @param addressStr The address as a string.
|
||||
* @return A map containing the decompilation result, or an empty map if not found.
|
||||
*/
|
||||
public static Map<String, Object> decompileFunction(Program program, String addressStr) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
if (program == null || addressStr == null || addressStr.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
AddressFactory addressFactory = program.getAddressFactory();
|
||||
Address address;
|
||||
|
||||
try {
|
||||
address = addressFactory.getAddress(addressStr);
|
||||
} catch (Exception e) {
|
||||
Msg.error(GhidraUtil.class, "Invalid address format: " + addressStr, e);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (address == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
FunctionManager functionManager = program.getFunctionManager();
|
||||
Function function = functionManager.getFunctionAt(address);
|
||||
|
||||
if (function == null) {
|
||||
function = functionManager.getFunctionContaining(address);
|
||||
}
|
||||
|
||||
if (function == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
String decompilation = decompileFunction(function);
|
||||
result.put("decompilation", decompilation != null ? decompilation : "");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to decompile a function.
|
||||
* @param function The function to decompile.
|
||||
* @return The decompiled code as a string, or null if decompilation failed.
|
||||
*/
|
||||
private static String decompileFunction(Function function) {
|
||||
if (function == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Program program = function.getProgram();
|
||||
DecompInterface decompiler = new DecompInterface();
|
||||
DecompileOptions options = new DecompileOptions();
|
||||
|
||||
decompiler.setOptions(options);
|
||||
decompiler.openProgram(program);
|
||||
|
||||
try {
|
||||
DecompileResults results = decompiler.decompileFunction(function, 30, TaskMonitor.DUMMY);
|
||||
if (results.decompileCompleted()) {
|
||||
return results.getDecompiledFunction().getC();
|
||||
} else {
|
||||
Msg.warn(GhidraUtil.class, "Decompilation failed for function: " + function.getName());
|
||||
return "// Decompilation failed for " + function.getName();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(GhidraUtil.class, "Error during decompilation of function: " + function.getName(), e);
|
||||
return "// Error during decompilation: " + e.getMessage();
|
||||
} finally {
|
||||
decompiler.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information about variables in a function.
|
||||
* @param function The function to get variables from.
|
||||
* @return A list of maps containing information about each variable.
|
||||
*/
|
||||
public static List<Map<String, Object>> getFunctionVariables(Function function) {
|
||||
List<Map<String, Object>> variables = new ArrayList<>();
|
||||
|
||||
if (function == null) {
|
||||
return variables;
|
||||
}
|
||||
|
||||
// Add parameters
|
||||
for (Parameter param : function.getParameters()) {
|
||||
Map<String, Object> varInfo = new HashMap<>();
|
||||
varInfo.put("name", param.getName());
|
||||
varInfo.put("type", param.getDataType().getName());
|
||||
varInfo.put("isParameter", true);
|
||||
variables.add(varInfo);
|
||||
}
|
||||
|
||||
// Add local variables
|
||||
for (Variable var : function.getAllVariables()) {
|
||||
if (var instanceof Parameter) {
|
||||
continue; // Skip parameters, already added
|
||||
}
|
||||
|
||||
Map<String, Object> varInfo = new HashMap<>();
|
||||
varInfo.put("name", var.getName());
|
||||
varInfo.put("type", var.getDataType().getName());
|
||||
varInfo.put("isParameter", false);
|
||||
variables.add(varInfo);
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
}
|
||||
123
src/main/java/eu/starsong/ghidra/util/HttpUtil.java
Normal file
123
src/main/java/eu/starsong/ghidra/util/HttpUtil.java
Normal file
@ -0,0 +1,123 @@
|
||||
package eu.starsong.ghidra.util;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import eu.starsong.ghidra.api.ResponseBuilder; // Use the ResponseBuilder
|
||||
import ghidra.util.Msg;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class HttpUtil {
|
||||
|
||||
private static final Gson gson = new Gson();
|
||||
|
||||
/**
|
||||
* Sends a JSON response with the given status code.
|
||||
* Uses the ResponseBuilder internally.
|
||||
*/
|
||||
public static void sendJsonResponse(HttpExchange exchange, JsonObject jsonObj, int statusCode, int port) throws IOException {
|
||||
try {
|
||||
String json = gson.toJson(jsonObj);
|
||||
byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
||||
// Consider adding CORS headers if needed:
|
||||
// exchange.getResponseHeaders().set("Access-Control-Allow-Origin", "*");
|
||||
|
||||
long responseLength = (statusCode == 204) ? -1 : bytes.length;
|
||||
exchange.sendResponseHeaders(statusCode, responseLength);
|
||||
|
||||
if (responseLength != -1) {
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(bytes);
|
||||
}
|
||||
} else {
|
||||
exchange.getResponseBody().close(); // Important for 204
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(HttpUtil.class, "Error sending JSON response: " + e.getMessage(), e);
|
||||
// Avoid sending another error response here to prevent potential loops
|
||||
if (!exchange.getResponseHeaders().containsKey("Content-Type")) {
|
||||
byte[] errorBytes = ("Internal Server Error: " + e.getMessage()).getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
|
||||
exchange.sendResponseHeaders(500, errorBytes.length);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(errorBytes);
|
||||
} catch (IOException writeErr) {
|
||||
Msg.error(HttpUtil.class, "Failed to send even plain text error response", writeErr);
|
||||
}
|
||||
}
|
||||
throw new IOException("Failed to send JSON response", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a standardized error response using ResponseBuilder.
|
||||
*/
|
||||
public static void sendErrorResponse(HttpExchange exchange, int statusCode, String message, String errorCode, int port) throws IOException {
|
||||
ResponseBuilder builder = new ResponseBuilder(exchange, port)
|
||||
.success(false)
|
||||
.error(message, errorCode);
|
||||
sendJsonResponse(exchange, builder.build(), statusCode, port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses query parameters from the URL.
|
||||
*/
|
||||
public static Map<String, String> parseQueryParams(HttpExchange exchange) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
String query = exchange.getRequestURI().getQuery();
|
||||
if (query != null) {
|
||||
String[] pairs = query.split("&");
|
||||
for (String p : pairs) {
|
||||
String[] kv = p.split("=");
|
||||
if (kv.length == 2) {
|
||||
try {
|
||||
result.put(kv[0], java.net.URLDecoder.decode(kv[1], StandardCharsets.UTF_8));
|
||||
} catch (Exception e) {
|
||||
Msg.warn(HttpUtil.class, "Failed to decode query parameter: " + kv[0]);
|
||||
result.put(kv[0], kv[1]);
|
||||
}
|
||||
} else if (kv.length == 1 && !kv[0].isEmpty()) {
|
||||
result.put(kv[0], "");
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses POST body parameters strictly as JSON.
|
||||
*/
|
||||
public static Map<String, String> parseJsonPostParams(HttpExchange exchange) throws IOException {
|
||||
byte[] body = exchange.getRequestBody().readAllBytes();
|
||||
String bodyStr = new String(body, StandardCharsets.UTF_8);
|
||||
Map<String, String> params = new HashMap<>();
|
||||
|
||||
try {
|
||||
JsonObject json = gson.fromJson(bodyStr, JsonObject.class);
|
||||
if (json == null) {
|
||||
return params;
|
||||
}
|
||||
for (Map.Entry<String, JsonElement> entry : json.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
JsonElement value = entry.getValue();
|
||||
if (value.isJsonPrimitive()) {
|
||||
params.put(key, value.getAsString());
|
||||
} else {
|
||||
params.put(key, value.toString()); // Stringify non-primitives
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(HttpUtil.class, "Failed to parse JSON request body: " + bodyStr, e);
|
||||
throw new IOException("Invalid JSON request body: " + e.getMessage(), e);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
}
|
||||
59
src/main/java/eu/starsong/ghidra/util/TransactionHelper.java
Normal file
59
src/main/java/eu/starsong/ghidra/util/TransactionHelper.java
Normal file
@ -0,0 +1,59 @@
|
||||
package eu.starsong.ghidra.util;
|
||||
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.util.Msg;
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public class TransactionHelper {
|
||||
|
||||
@FunctionalInterface
|
||||
public interface GhidraSupplier<T> {
|
||||
T get() throws Exception;
|
||||
}
|
||||
|
||||
public static <T> T executeInTransaction(Program program, String transactionName, GhidraSupplier<T> operation)
|
||||
throws TransactionException {
|
||||
|
||||
if (program == null) {
|
||||
throw new IllegalArgumentException("Program cannot be null for transaction");
|
||||
}
|
||||
|
||||
AtomicReference<T> result = new AtomicReference<>();
|
||||
AtomicReference<Exception> exception = new AtomicReference<>();
|
||||
|
||||
try {
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
int txId = -1;
|
||||
boolean success = false;
|
||||
try {
|
||||
txId = program.startTransaction(transactionName);
|
||||
if (txId < 0) {
|
||||
throw new TransactionException("Failed to start transaction: " + transactionName);
|
||||
}
|
||||
result.set(operation.get());
|
||||
success = true;
|
||||
} catch (Exception e) {
|
||||
exception.set(e);
|
||||
Msg.error(TransactionHelper.class, "Transaction failed: " + transactionName, e);
|
||||
} finally {
|
||||
if (txId >= 0) {
|
||||
program.endTransaction(txId, success);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
throw new TransactionException("Swing thread execution failed", e);
|
||||
}
|
||||
|
||||
if (exception.get() != null) {
|
||||
throw new TransactionException("Operation failed", exception.get());
|
||||
}
|
||||
return result.get();
|
||||
}
|
||||
|
||||
public static class TransactionException extends Exception {
|
||||
public TransactionException(String message) { super(message); }
|
||||
public TransactionException(String message, Throwable cause) { super(message, cause); }
|
||||
}
|
||||
}
|
||||
@ -26,10 +26,8 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
"""Helper to assert the standard success response structure."""
|
||||
self.assertIn("success", data, "Response missing 'success' field")
|
||||
self.assertTrue(data["success"], f"API call failed: {data.get('error', 'Unknown error')}")
|
||||
self.assertIn("timestamp", data, "Response missing 'timestamp' field")
|
||||
self.assertIsInstance(data["timestamp"], (int, float), "'timestamp' should be a number")
|
||||
self.assertIn("port", data, "Response missing 'port' field")
|
||||
self.assertEqual(data["port"], DEFAULT_PORT, f"Response port mismatch: expected {DEFAULT_PORT}, got {data['port']}")
|
||||
self.assertIn("id", data, "Response missing 'id' field")
|
||||
self.assertIn("instance", data, "Response missing 'instance' field")
|
||||
self.assertIn("result", data, "Response missing 'result' field")
|
||||
if expected_result_type:
|
||||
self.assertIsInstance(data["result"], expected_result_type, f"'result' field type mismatch: expected {expected_result_type}, got {type(data['result'])}")
|
||||
@ -52,11 +50,14 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
# Verify response is valid JSON
|
||||
data = response.json()
|
||||
|
||||
# Check required fields
|
||||
self.assertIn("port", data)
|
||||
self.assertIn("isBaseInstance", data)
|
||||
self.assertIn("project", data)
|
||||
self.assertIn("file", data)
|
||||
# Check standard response structure
|
||||
self.assertStandardSuccessResponse(data, expected_result_type=dict)
|
||||
|
||||
# Check required fields in result
|
||||
result = data["result"]
|
||||
self.assertIn("isBaseInstance", result)
|
||||
self.assertIn("project", result)
|
||||
self.assertIn("file", result)
|
||||
|
||||
def test_root_endpoint(self):
|
||||
"""Test the / endpoint"""
|
||||
@ -66,11 +67,13 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
# Verify response is valid JSON
|
||||
data = response.json()
|
||||
|
||||
# Check required fields
|
||||
self.assertIn("port", data)
|
||||
self.assertIn("isBaseInstance", data)
|
||||
self.assertIn("project", data)
|
||||
self.assertIn("file", data)
|
||||
# Check standard response structure
|
||||
self.assertStandardSuccessResponse(data, expected_result_type=dict)
|
||||
|
||||
# Check required fields in result
|
||||
result = data["result"]
|
||||
self.assertIn("isBaseInstance", result)
|
||||
self.assertIn("message", result)
|
||||
|
||||
def test_instances_endpoint(self):
|
||||
"""Test the /instances endpoint"""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user