WIP big refactor

This commit is contained in:
Teal Bauer 2025-04-10 14:42:53 +02:00
parent 454c73908c
commit 9879e71e88
24 changed files with 3284 additions and 2909 deletions

View File

@ -17,12 +17,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Multiple simplification styles - Multiple simplification styles
- Comprehensive API documentation (GHIDRA_HTTP_API.md, MCP_BRIDGE_API.md) - Comprehensive API documentation (GHIDRA_HTTP_API.md, MCP_BRIDGE_API.md)
- Standardized JSON response formats - Standardized JSON response formats
- Implemented `/plugin-version` endpoint for version checking
- Added proper error handling for when no program is loaded
### Changed ### Changed
- Unified all endpoints to use structured JSON - Unified all endpoints to use structured JSON
- Improved error handling and response metadata - Improved error handling and response metadata
- Simplified bridge code and added type hints - Simplified bridge code and added type hints
- Updated port discovery to use DEFAULT_GHIDRA_PORT - 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 ## [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

View 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;
}

View File

@ -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);
}

View 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;
}
}

View File

@ -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
}

View File

@ -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
}

View 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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View 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
}

View File

@ -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
}

View 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();
}
}
}

View 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;
}
}
}

View 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();
}
}

View 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();
}
}

View File

@ -0,0 +1,6 @@
package eu.starsong.ghidra.util;
@FunctionalInterface
public interface GhidraSupplier<T> {
T get() throws Exception;
}

View 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;
}
}

View 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;
}
}

View 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); }
}
}

View File

@ -26,10 +26,8 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
"""Helper to assert the standard success response structure.""" """Helper to assert the standard success response structure."""
self.assertIn("success", data, "Response missing 'success' field") self.assertIn("success", data, "Response missing 'success' field")
self.assertTrue(data["success"], f"API call failed: {data.get('error', 'Unknown error')}") self.assertTrue(data["success"], f"API call failed: {data.get('error', 'Unknown error')}")
self.assertIn("timestamp", data, "Response missing 'timestamp' field") self.assertIn("id", data, "Response missing 'id' field")
self.assertIsInstance(data["timestamp"], (int, float), "'timestamp' should be a number") self.assertIn("instance", data, "Response missing 'instance' field")
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("result", data, "Response missing 'result' field") self.assertIn("result", data, "Response missing 'result' field")
if expected_result_type: if expected_result_type:
self.assertIsInstance(data["result"], expected_result_type, f"'result' field type mismatch: expected {expected_result_type}, got {type(data['result'])}") 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 # Verify response is valid JSON
data = response.json() data = response.json()
# Check required fields # Check standard response structure
self.assertIn("port", data) self.assertStandardSuccessResponse(data, expected_result_type=dict)
self.assertIn("isBaseInstance", data)
self.assertIn("project", data) # Check required fields in result
self.assertIn("file", data) result = data["result"]
self.assertIn("isBaseInstance", result)
self.assertIn("project", result)
self.assertIn("file", result)
def test_root_endpoint(self): def test_root_endpoint(self):
"""Test the / endpoint""" """Test the / endpoint"""
@ -66,11 +67,13 @@ class GhydraMCPHttpApiTests(unittest.TestCase):
# Verify response is valid JSON # Verify response is valid JSON
data = response.json() data = response.json()
# Check required fields # Check standard response structure
self.assertIn("port", data) self.assertStandardSuccessResponse(data, expected_result_type=dict)
self.assertIn("isBaseInstance", data)
self.assertIn("project", data) # Check required fields in result
self.assertIn("file", data) result = data["result"]
self.assertIn("isBaseInstance", result)
self.assertIn("message", result)
def test_instances_endpoint(self): def test_instances_endpoint(self):
"""Test the /instances endpoint""" """Test the /instances endpoint"""