diff --git a/GHIDRA_HTTP_API.md b/GHIDRA_HTTP_API.md index 0608188..8d8d83b 100644 --- a/GHIDRA_HTTP_API.md +++ b/GHIDRA_HTTP_API.md @@ -404,6 +404,201 @@ Provides access to string data in the binary. } ``` +### 6.2 Structs + +Provides functionality for creating and managing struct (composite) data types. + +- **`GET /structs`**: List all struct data types in the program. Supports pagination and filtering. + - Query Parameters: + - `?offset=[int]`: Number of structs to skip (default: 0). + - `?limit=[int]`: Maximum number of structs to return (default: 100). + - `?category=[string]`: Filter by category path (e.g. "/winapi"). + ```json + // Example Response + "result": [ + { + "name": "MyStruct", + "path": "/custom/MyStruct", + "size": 16, + "numFields": 4, + "category": "/custom", + "description": "Custom data structure" + }, + { + "name": "FileHeader", + "path": "/FileHeader", + "size": 32, + "numFields": 8, + "category": "/", + "description": "" + } + ], + "_links": { + "self": { "href": "/structs?offset=0&limit=100" }, + "program": { "href": "/program" } + } + ``` + +- **`GET /structs?name={struct_name}`**: Get detailed information about a specific struct including all fields. + ```json + // Example Response for GET /structs?name=MyStruct + "result": { + "name": "MyStruct", + "path": "/custom/MyStruct", + "size": 16, + "category": "/custom", + "description": "Custom data structure", + "numFields": 4, + "fields": [ + { + "name": "id", + "offset": 0, + "length": 4, + "type": "int", + "typePath": "/int", + "comment": "Unique identifier" + }, + { + "name": "flags", + "offset": 4, + "length": 4, + "type": "dword", + "typePath": "/dword", + "comment": "" + }, + { + "name": "data_ptr", + "offset": 8, + "length": 4, + "type": "pointer", + "typePath": "/pointer", + "comment": "Pointer to data" + }, + { + "name": "size", + "offset": 12, + "length": 4, + "type": "uint", + "typePath": "/uint", + "comment": "" + } + ] + }, + "_links": { + "self": { "href": "/structs?name=MyStruct" }, + "structs": { "href": "/structs" }, + "program": { "href": "/program" } + } + ``` + +- **`POST /structs/create`**: Create a new struct data type. + - Request Payload: + - `name`: Name for the new struct (required). + - `category`: Category path (optional, defaults to root). + - `description`: Description for the struct (optional). + ```json + // Example Request Payload + { + "name": "NetworkPacket", + "category": "/network", + "description": "Network packet structure" + } + + // Example Response + "result": { + "name": "NetworkPacket", + "path": "/network/NetworkPacket", + "category": "/network", + "size": 0, + "message": "Struct created successfully" + } + ``` + +- **`POST /structs/addfield`**: Add a field to an existing struct. + - Request Payload: + - `struct`: Name of the struct to modify (required). + - `fieldName`: Name for the new field (required). + - `fieldType`: Data type for the field (required, e.g. "int", "char", "pointer"). + - `offset`: Specific offset to insert field (optional, appends to end if not specified). + - `comment`: Comment for the field (optional). + ```json + // Example Request Payload + { + "struct": "NetworkPacket", + "fieldName": "header", + "fieldType": "dword", + "comment": "Packet header" + } + + // Example Response + "result": { + "struct": "NetworkPacket", + "fieldName": "header", + "fieldType": "dword", + "offset": 0, + "length": 4, + "structSize": 4, + "message": "Field added successfully" + } + ``` + +- **`POST /structs/updatefield`**: Update an existing field in a struct (rename, change type, or modify comment). + - Request Payload: + - `struct`: Name of the struct to modify (required). + - `fieldOffset` OR `fieldName`: Identify the field to update (one required). + - `newName`: New name for the field (optional). + - `newType`: New data type for the field (optional). + - `newComment`: New comment for the field (optional). + - At least one of `newName`, `newType`, or `newComment` must be provided. + ```json + // Example Request Payload - rename a field + { + "struct": "NetworkPacket", + "fieldName": "header", + "newName": "packet_header", + "newComment": "Updated packet header field" + } + + // Example Request Payload - change type by offset + { + "struct": "NetworkPacket", + "fieldOffset": 0, + "newType": "qword" + } + + // Example Response + "result": { + "struct": "NetworkPacket", + "offset": 0, + "originalName": "header", + "originalType": "dword", + "originalComment": "Packet header", + "newName": "packet_header", + "newType": "dword", + "newComment": "Updated packet header field", + "length": 4, + "message": "Field updated successfully" + } + ``` + +- **`POST /structs/delete`**: Delete a struct data type. + - Request Payload: + - `name`: Name of the struct to delete (required). + ```json + // Example Request Payload + { + "name": "NetworkPacket" + } + + // Example Response + "result": { + "name": "NetworkPacket", + "path": "/network/NetworkPacket", + "category": "/network", + "message": "Struct deleted successfully" + } + ``` + ### 7. Memory Segments Represents memory blocks/sections defined in the program. diff --git a/bridge_mcp_hydra.py b/bridge_mcp_hydra.py index 590a3c0..879b139 100644 --- a/bridge_mcp_hydra.py +++ b/bridge_mcp_hydra.py @@ -48,6 +48,7 @@ The API is organized into namespaces for different types of operations: - instances_* : For managing Ghidra instances - functions_* : For working with functions - data_* : For working with data items +- structs_* : For creating and managing struct data types - memory_* : For memory access - xrefs_* : For cross-references - analysis_* : For program analysis @@ -1924,6 +1925,234 @@ def data_set_type(address: str, data_type: str, port: int = None) -> dict: response = safe_post(port, "data/type", payload) return simplify_response(response) +# Struct tools +@mcp.tool() +def structs_list(offset: int = 0, limit: int = 100, category: str = None, port: int = None) -> dict: + """List all struct data types in the program + + Args: + offset: Pagination offset (default: 0) + limit: Maximum items to return (default: 100) + category: Filter by category path (e.g. "/winapi") + port: Specific Ghidra instance port (optional) + + Returns: + dict: List of structs with name, size, and field count + """ + port = _get_instance_port(port) + + params = { + "offset": offset, + "limit": limit + } + if category: + params["category"] = category + + response = safe_get(port, "structs", params) + simplified = simplify_response(response) + + # Ensure we maintain pagination metadata + if isinstance(simplified, dict) and "error" not in simplified: + simplified.setdefault("size", len(simplified.get("result", []))) + simplified.setdefault("offset", offset) + simplified.setdefault("limit", limit) + + return simplified + +@mcp.tool() +def structs_get(name: str, port: int = None) -> dict: + """Get detailed information about a specific struct including all fields + + Args: + name: Struct name + port: Specific Ghidra instance port (optional) + + Returns: + dict: Struct details including all fields with their names, types, and offsets + """ + if not name: + return { + "success": False, + "error": { + "code": "MISSING_PARAMETER", + "message": "Struct name parameter is required" + }, + "timestamp": int(time.time() * 1000) + } + + port = _get_instance_port(port) + + params = {"name": name} + response = safe_get(port, "structs", params) + return simplify_response(response) + +@mcp.tool() +def structs_create(name: str, category: str = None, description: str = None, port: int = None) -> dict: + """Create a new struct data type + + Args: + name: Name for the new struct + category: Category path for the struct (e.g. "/custom") + description: Optional description for the struct + port: Specific Ghidra instance port (optional) + + Returns: + dict: Created struct information + """ + if not name: + return { + "success": False, + "error": { + "code": "MISSING_PARAMETER", + "message": "Struct name parameter is required" + }, + "timestamp": int(time.time() * 1000) + } + + port = _get_instance_port(port) + + payload = {"name": name} + if category: + payload["category"] = category + if description: + payload["description"] = description + + response = safe_post(port, "structs/create", payload) + return simplify_response(response) + +@mcp.tool() +def structs_add_field(struct_name: str, field_name: str, field_type: str, + offset: int = None, comment: str = None, port: int = None) -> dict: + """Add a field to an existing struct + + Args: + struct_name: Name of the struct to modify + field_name: Name for the new field + field_type: Data type for the field (e.g. "int", "char", "pointer") + offset: Specific offset to insert field (optional, appends to end if not specified) + comment: Optional comment for the field + port: Specific Ghidra instance port (optional) + + Returns: + dict: Operation result with updated struct size and field information + """ + if not struct_name or not field_name or not field_type: + return { + "success": False, + "error": { + "code": "MISSING_PARAMETER", + "message": "struct_name, field_name, and field_type parameters are required" + }, + "timestamp": int(time.time() * 1000) + } + + port = _get_instance_port(port) + + payload = { + "struct": struct_name, + "fieldName": field_name, + "fieldType": field_type + } + if offset is not None: + payload["offset"] = offset + if comment: + payload["comment"] = comment + + response = safe_post(port, "structs/addfield", payload) + return simplify_response(response) + +@mcp.tool() +def structs_update_field(struct_name: str, field_name: str = None, field_offset: int = None, + new_name: str = None, new_type: str = None, new_comment: str = None, + port: int = None) -> dict: + """Update an existing field in a struct (change name, type, or comment) + + Args: + struct_name: Name of the struct to modify + field_name: Name of the field to update (use this OR field_offset) + field_offset: Offset of the field to update (use this OR field_name) + new_name: New name for the field (optional) + new_type: New data type for the field (optional, e.g. "int", "pointer") + new_comment: New comment for the field (optional) + port: Specific Ghidra instance port (optional) + + Returns: + dict: Operation result with old and new field values + """ + if not struct_name: + return { + "success": False, + "error": { + "code": "MISSING_PARAMETER", + "message": "struct_name parameter is required" + }, + "timestamp": int(time.time() * 1000) + } + + if not field_name and field_offset is None: + return { + "success": False, + "error": { + "code": "MISSING_PARAMETER", + "message": "Either field_name or field_offset must be provided" + }, + "timestamp": int(time.time() * 1000) + } + + if not new_name and not new_type and new_comment is None: + return { + "success": False, + "error": { + "code": "MISSING_PARAMETER", + "message": "At least one of new_name, new_type, or new_comment must be provided" + }, + "timestamp": int(time.time() * 1000) + } + + port = _get_instance_port(port) + + payload = {"struct": struct_name} + if field_name: + payload["fieldName"] = field_name + if field_offset is not None: + payload["fieldOffset"] = field_offset + if new_name: + payload["newName"] = new_name + if new_type: + payload["newType"] = new_type + if new_comment is not None: + payload["newComment"] = new_comment + + response = safe_post(port, "structs/updatefield", payload) + return simplify_response(response) + +@mcp.tool() +def structs_delete(name: str, port: int = None) -> dict: + """Delete a struct data type + + Args: + name: Name of the struct to delete + port: Specific Ghidra instance port (optional) + + Returns: + dict: Operation result confirming deletion + """ + if not name: + return { + "success": False, + "error": { + "code": "MISSING_PARAMETER", + "message": "Struct name parameter is required" + }, + "timestamp": int(time.time() * 1000) + } + + port = _get_instance_port(port) + + payload = {"name": name} + response = safe_post(port, "structs/delete", payload) + return simplify_response(response) + # Analysis tools @mcp.tool() def analysis_run(port: int = None, analysis_options: dict = None) -> dict: diff --git a/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java b/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java index 1087c1f..d5c3082 100644 --- a/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java +++ b/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java @@ -139,6 +139,7 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { new SymbolEndpoints(currentProgram, port, tool).registerEndpoints(server); new NamespaceEndpoints(currentProgram, port, tool).registerEndpoints(server); new DataEndpoints(currentProgram, port, tool).registerEndpoints(server); + new StructEndpoints(currentProgram, port, tool).registerEndpoints(server); new MemoryEndpoints(currentProgram, port, tool).registerEndpoints(server); new XrefsEndpoints(currentProgram, port, tool).registerEndpoints(server); new AnalysisEndpoints(currentProgram, port, tool).registerEndpoints(server); @@ -376,6 +377,7 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { .addLink("data", "/data") .addLink("strings", "/strings") .addLink("segments", "/segments") + .addLink("structs", "/structs") .addLink("memory", "/memory") .addLink("xrefs", "/xrefs") .addLink("analysis", "/analysis") diff --git a/src/main/java/eu/starsong/ghidra/endpoints/StructEndpoints.java b/src/main/java/eu/starsong/ghidra/endpoints/StructEndpoints.java new file mode 100644 index 0000000..a43d834 --- /dev/null +++ b/src/main/java/eu/starsong/ghidra/endpoints/StructEndpoints.java @@ -0,0 +1,776 @@ +package eu.starsong.ghidra.endpoints; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import eu.starsong.ghidra.api.ResponseBuilder; +import eu.starsong.ghidra.util.TransactionHelper; +import eu.starsong.ghidra.util.TransactionHelper.TransactionException; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.data.*; +import ghidra.program.model.listing.Program; +import ghidra.util.Msg; + +import java.io.IOException; +import java.util.*; + +/** + * Endpoints for managing struct (composite) data types in Ghidra. + * Provides REST API for creating, listing, modifying, and deleting structs. + */ +public class StructEndpoints extends AbstractEndpoint { + + private PluginTool tool; + + public StructEndpoints(Program program, int port) { + super(program, port); + } + + public StructEndpoints(Program program, int port, PluginTool tool) { + super(program, port); + this.tool = tool; + } + + @Override + protected PluginTool getTool() { + return tool; + } + + @Override + public void registerEndpoints(HttpServer server) { + server.createContext("/structs", this::handleStructs); + server.createContext("/structs/create", exchange -> { + try { + if ("POST".equals(exchange.getRequestMethod())) { + Map params = parseJsonPostParams(exchange); + handleCreateStruct(exchange, params); + } else { + sendErrorResponse(exchange, 405, "Method Not Allowed"); + } + } catch (Exception e) { + Msg.error(this, "Error in /structs/create endpoint", e); + sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); + } + }); + server.createContext("/structs/delete", exchange -> { + try { + if ("POST".equals(exchange.getRequestMethod())) { + Map params = parseJsonPostParams(exchange); + handleDeleteStruct(exchange, params); + } else { + sendErrorResponse(exchange, 405, "Method Not Allowed"); + } + } catch (Exception e) { + Msg.error(this, "Error in /structs/delete endpoint", e); + sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); + } + }); + server.createContext("/structs/addfield", exchange -> { + try { + if ("POST".equals(exchange.getRequestMethod())) { + Map params = parseJsonPostParams(exchange); + handleAddField(exchange, params); + } else { + sendErrorResponse(exchange, 405, "Method Not Allowed"); + } + } catch (Exception e) { + Msg.error(this, "Error in /structs/addfield endpoint", e); + sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); + } + }); + server.createContext("/structs/updatefield", exchange -> { + try { + if ("POST".equals(exchange.getRequestMethod()) || "PATCH".equals(exchange.getRequestMethod())) { + Map params = parseJsonPostParams(exchange); + handleUpdateField(exchange, params); + } else { + sendErrorResponse(exchange, 405, "Method Not Allowed"); + } + } catch (Exception e) { + Msg.error(this, "Error in /structs/updatefield endpoint", e); + sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); + } + }); + } + + /** + * Handle GET /structs - list all structs, or GET /structs?name=X - get specific struct details + */ + private void handleStructs(HttpExchange exchange) throws IOException { + try { + if ("GET".equals(exchange.getRequestMethod())) { + Map qparams = parseQueryParams(exchange); + String structName = qparams.get("name"); + + if (structName != null && !structName.isEmpty()) { + handleGetStruct(exchange, structName); + } else { + handleListStructs(exchange); + } + } else { + sendErrorResponse(exchange, 405, "Method Not Allowed"); + } + } catch (Exception e) { + Msg.error(this, "Error in /structs endpoint", e); + sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); + } + } + + /** + * List all struct data types in the program + */ + private void handleListStructs(HttpExchange exchange) throws IOException { + try { + Map qparams = parseQueryParams(exchange); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + String categoryFilter = qparams.get("category"); + + Program program = getCurrentProgram(); + if (program == null) { + sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED"); + return; + } + + DataTypeManager dtm = program.getDataTypeManager(); + List> structList = new ArrayList<>(); + + // Iterate through all data types and filter for structures + dtm.getAllDataTypes().forEachRemaining(dataType -> { + if (dataType instanceof Structure) { + Structure struct = (Structure) dataType; + + // Apply category filter if specified + if (categoryFilter != null && !categoryFilter.isEmpty()) { + CategoryPath catPath = struct.getCategoryPath(); + if (!catPath.getPath().contains(categoryFilter)) { + return; + } + } + + Map structInfo = new HashMap<>(); + structInfo.put("name", struct.getName()); + structInfo.put("path", struct.getPathName()); + structInfo.put("size", struct.getLength()); + structInfo.put("numFields", struct.getNumComponents()); + structInfo.put("category", struct.getCategoryPath().getPath()); + structInfo.put("description", struct.getDescription() != null ? struct.getDescription() : ""); + + // Add HATEOAS links + Map links = new HashMap<>(); + Map selfLink = new HashMap<>(); + selfLink.put("href", "/structs?name=" + struct.getName()); + links.put("self", selfLink); + structInfo.put("_links", links); + + structList.add(structInfo); + } + }); + + // Sort by name for consistency + structList.sort(Comparator.comparing(s -> (String) s.get("name"))); + + // Build response with pagination + ResponseBuilder builder = new ResponseBuilder(exchange, port).success(true); + List> paginated = applyPagination(structList, offset, limit, builder, "/structs"); + builder.result(paginated); + builder.addLink("program", "/program"); + + sendJsonResponse(exchange, builder.build(), 200); + } catch (Exception e) { + Msg.error(this, "Error listing structs", e); + sendErrorResponse(exchange, 500, "Error listing structs: " + e.getMessage(), "INTERNAL_ERROR"); + } + } + + /** + * Get details of a specific struct including all fields + */ + private void handleGetStruct(HttpExchange exchange, String structName) throws IOException { + try { + Program program = getCurrentProgram(); + if (program == null) { + sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED"); + return; + } + + DataTypeManager dtm = program.getDataTypeManager(); + + // Try to find the struct - support both full paths and simple names + DataType dataType = null; + + // If it looks like a full path (starts with /), try direct lookup + if (structName.startsWith("/")) { + dataType = dtm.getDataType(structName); + if (dataType == null) { + dataType = dtm.findDataType(structName); + } + } else { + // Search by simple name using the helper method + dataType = findStructByName(dtm, structName); + } + + if (dataType == null || !(dataType instanceof Structure)) { + sendErrorResponse(exchange, 404, "Struct not found: " + structName, "STRUCT_NOT_FOUND"); + return; + } + + Structure struct = (Structure) dataType; + Map structInfo = buildStructInfo(struct); + + ResponseBuilder builder = new ResponseBuilder(exchange, port) + .success(true) + .result(structInfo); + + builder.addLink("self", "/structs?name=" + struct.getName()); + builder.addLink("structs", "/structs"); + builder.addLink("program", "/program"); + + sendJsonResponse(exchange, builder.build(), 200); + } catch (Exception e) { + Msg.error(this, "Error getting struct details", e); + sendErrorResponse(exchange, 500, "Error getting struct: " + e.getMessage(), "INTERNAL_ERROR"); + } + } + + /** + * Create a new struct data type + * POST /structs/create + * Required params: name + * Optional params: category, size, description + */ + private void handleCreateStruct(HttpExchange exchange, Map params) throws IOException { + try { + String structName = params.get("name"); + String category = params.get("category"); + String sizeStr = params.get("size"); + String description = params.get("description"); + + if (structName == null || structName.isEmpty()) { + sendErrorResponse(exchange, 400, "Missing required parameter: name", "MISSING_PARAMETERS"); + return; + } + + Program program = getCurrentProgram(); + if (program == null) { + sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED"); + return; + } + + Map resultMap = new HashMap<>(); + resultMap.put("name", structName); + + try { + TransactionHelper.executeInTransaction(program, "Create Struct", () -> { + DataTypeManager dtm = program.getDataTypeManager(); + + // Check if struct already exists + DataType existing = dtm.getDataType("/" + structName); + if (existing != null) { + throw new Exception("Struct already exists: " + structName); + } + + // Determine category path + CategoryPath catPath; + if (category != null && !category.isEmpty()) { + catPath = new CategoryPath(category); + } else { + catPath = CategoryPath.ROOT; + } + + // Create the structure + StructureDataType struct = new StructureDataType(catPath, structName, 0); + + if (description != null && !description.isEmpty()) { + struct.setDescription(description); + } + + // Add to data type manager + Structure addedStruct = (Structure) dtm.addDataType(struct, DataTypeConflictHandler.DEFAULT_HANDLER); + + resultMap.put("path", addedStruct.getPathName()); + resultMap.put("category", addedStruct.getCategoryPath().getPath()); + resultMap.put("size", addedStruct.getLength()); + + return null; + }); + + resultMap.put("message", "Struct created successfully"); + + ResponseBuilder builder = new ResponseBuilder(exchange, port) + .success(true) + .result(resultMap); + + builder.addLink("self", "/structs?name=" + structName); + builder.addLink("structs", "/structs"); + builder.addLink("program", "/program"); + + sendJsonResponse(exchange, builder.build(), 201); + } catch (TransactionException e) { + Msg.error(this, "Transaction failed: Create Struct", e); + sendErrorResponse(exchange, 500, "Failed to create struct: " + e.getMessage(), "TRANSACTION_ERROR"); + } catch (Exception e) { + Msg.error(this, "Error creating struct", e); + sendErrorResponse(exchange, 400, "Error creating struct: " + e.getMessage(), "INVALID_PARAMETER"); + } + } catch (Exception e) { + Msg.error(this, "Unexpected error creating struct", e); + sendErrorResponse(exchange, 500, "Error creating struct: " + e.getMessage(), "INTERNAL_ERROR"); + } + } + + /** + * Add a field to an existing struct + * POST /structs/addfield + * Required params: struct, fieldName, fieldType + * Optional params: offset, comment + */ + private void handleAddField(HttpExchange exchange, Map params) throws IOException { + try { + String structName = params.get("struct"); + String fieldName = params.get("fieldName"); + String fieldType = params.get("fieldType"); + String offsetStr = params.get("offset"); + String comment = params.get("comment"); + + if (structName == null || structName.isEmpty()) { + sendErrorResponse(exchange, 400, "Missing required parameter: struct", "MISSING_PARAMETERS"); + return; + } + if (fieldName == null || fieldName.isEmpty()) { + sendErrorResponse(exchange, 400, "Missing required parameter: fieldName", "MISSING_PARAMETERS"); + return; + } + if (fieldType == null || fieldType.isEmpty()) { + sendErrorResponse(exchange, 400, "Missing required parameter: fieldType", "MISSING_PARAMETERS"); + return; + } + + Program program = getCurrentProgram(); + if (program == null) { + sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED"); + return; + } + + Integer offset = null; + if (offsetStr != null && !offsetStr.isEmpty()) { + try { + offset = Integer.parseInt(offsetStr); + } catch (NumberFormatException e) { + sendErrorResponse(exchange, 400, "Invalid offset parameter: must be an integer", "INVALID_PARAMETER"); + return; + } + } + + Map resultMap = new HashMap<>(); + resultMap.put("struct", structName); + resultMap.put("fieldName", fieldName); + resultMap.put("fieldType", fieldType); + + final Integer finalOffset = offset; + + try { + TransactionHelper.executeInTransaction(program, "Add Struct Field", () -> { + DataTypeManager dtm = program.getDataTypeManager(); + + // Find the struct - handle both full paths and simple names + DataType dataType = null; + if (structName.startsWith("/")) { + dataType = dtm.getDataType(structName); + if (dataType == null) { + dataType = dtm.findDataType(structName); + } + } else { + dataType = findStructByName(dtm, structName); + } + + if (dataType == null || !(dataType instanceof Structure)) { + throw new Exception("Struct not found: " + structName); + } + + Structure struct = (Structure) dataType; + + // Find the field type + DataType fieldDataType = findDataType(dtm, fieldType); + if (fieldDataType == null) { + throw new Exception("Field type not found: " + fieldType); + } + + // Add the field + DataTypeComponent component; + if (finalOffset != null) { + // Insert at specific offset + component = struct.insertAtOffset(finalOffset, fieldDataType, + fieldDataType.getLength(), fieldName, comment); + } else { + // Append to end + component = struct.add(fieldDataType, fieldName, comment); + } + + resultMap.put("offset", component.getOffset()); + resultMap.put("length", component.getLength()); + resultMap.put("structSize", struct.getLength()); + + return null; + }); + + resultMap.put("message", "Field added successfully"); + + ResponseBuilder builder = new ResponseBuilder(exchange, port) + .success(true) + .result(resultMap); + + builder.addLink("struct", "/structs?name=" + structName); + builder.addLink("structs", "/structs"); + builder.addLink("program", "/program"); + + sendJsonResponse(exchange, builder.build(), 200); + } catch (TransactionException e) { + Msg.error(this, "Transaction failed: Add Struct Field", e); + sendErrorResponse(exchange, 500, "Failed to add field: " + e.getMessage(), "TRANSACTION_ERROR"); + } catch (Exception e) { + Msg.error(this, "Error adding field", e); + sendErrorResponse(exchange, 400, "Error adding field: " + e.getMessage(), "INVALID_PARAMETER"); + } + } catch (Exception e) { + Msg.error(this, "Unexpected error adding field", e); + sendErrorResponse(exchange, 500, "Error adding field: " + e.getMessage(), "INTERNAL_ERROR"); + } + } + + /** + * Update an existing field in a struct + * POST/PATCH /structs/updatefield + * Required params: struct, fieldOffset (or fieldName) + * Optional params: newName, newType, newComment + */ + private void handleUpdateField(HttpExchange exchange, Map params) throws IOException { + try { + String structName = params.get("struct"); + String fieldOffsetStr = params.get("fieldOffset"); + String fieldName = params.get("fieldName"); + String newName = params.get("newName"); + String newType = params.get("newType"); + String newComment = params.get("newComment"); + + if (structName == null || structName.isEmpty()) { + sendErrorResponse(exchange, 400, "Missing required parameter: struct", "MISSING_PARAMETERS"); + return; + } + + // Must have either fieldOffset or fieldName to identify the field + if ((fieldOffsetStr == null || fieldOffsetStr.isEmpty()) && (fieldName == null || fieldName.isEmpty())) { + sendErrorResponse(exchange, 400, "Missing required parameter: either fieldOffset or fieldName must be provided", "MISSING_PARAMETERS"); + return; + } + + // Must have at least one update parameter + if ((newName == null || newName.isEmpty()) && + (newType == null || newType.isEmpty()) && + (newComment == null || newComment.isEmpty())) { + sendErrorResponse(exchange, 400, "At least one of newName, newType, or newComment must be provided", "MISSING_PARAMETERS"); + return; + } + + Program program = getCurrentProgram(); + if (program == null) { + sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED"); + return; + } + + Integer fieldOffset = null; + if (fieldOffsetStr != null && !fieldOffsetStr.isEmpty()) { + try { + fieldOffset = Integer.parseInt(fieldOffsetStr); + } catch (NumberFormatException e) { + sendErrorResponse(exchange, 400, "Invalid fieldOffset parameter: must be an integer", "INVALID_PARAMETER"); + return; + } + } + + Map resultMap = new HashMap<>(); + resultMap.put("struct", structName); + + final Integer finalFieldOffset = fieldOffset; + final String finalFieldName = fieldName; + + try { + TransactionHelper.executeInTransaction(program, "Update Struct Field", () -> { + DataTypeManager dtm = program.getDataTypeManager(); + + // Find the struct + DataType dataType = null; + if (structName.startsWith("/")) { + dataType = dtm.getDataType(structName); + if (dataType == null) { + dataType = dtm.findDataType(structName); + } + } else { + dataType = findStructByName(dtm, structName); + } + + if (dataType == null || !(dataType instanceof Structure)) { + throw new Exception("Struct not found: " + structName); + } + + Structure struct = (Structure) dataType; + + // Find the field to update + DataTypeComponent component = null; + if (finalFieldOffset != null) { + component = struct.getComponentAt(finalFieldOffset); + } else { + // Search by field name + for (DataTypeComponent comp : struct.getComponents()) { + if (finalFieldName.equals(comp.getFieldName())) { + component = comp; + break; + } + } + } + + if (component == null) { + throw new Exception("Field not found in struct: " + (finalFieldOffset != null ? "offset " + finalFieldOffset : finalFieldName)); + } + + int componentOffset = component.getOffset(); + int componentLength = component.getLength(); + DataType originalType = component.getDataType(); + String originalName = component.getFieldName(); + String originalComment = component.getComment(); + + // Store original values + resultMap.put("originalName", originalName); + resultMap.put("originalType", originalType.getName()); + resultMap.put("originalComment", originalComment != null ? originalComment : ""); + resultMap.put("offset", componentOffset); + + // Determine new values + String updatedName = (newName != null && !newName.isEmpty()) ? newName : originalName; + String updatedComment = (newComment != null) ? newComment : originalComment; + DataType updatedType = originalType; + + if (newType != null && !newType.isEmpty()) { + updatedType = findDataType(dtm, newType); + if (updatedType == null) { + throw new Exception("Field type not found: " + newType); + } + } + + // Update the field by replacing it + // Ghidra doesn't have a direct "update" - we need to delete and re-add + struct.deleteAtOffset(componentOffset); + DataTypeComponent newComponent = struct.insertAtOffset(componentOffset, updatedType, + updatedType.getLength(), + updatedName, updatedComment); + + resultMap.put("newName", newComponent.getFieldName()); + resultMap.put("newType", newComponent.getDataType().getName()); + resultMap.put("newComment", newComponent.getComment() != null ? newComponent.getComment() : ""); + resultMap.put("length", newComponent.getLength()); + + return null; + }); + + resultMap.put("message", "Field updated successfully"); + + ResponseBuilder builder = new ResponseBuilder(exchange, port) + .success(true) + .result(resultMap); + + builder.addLink("struct", "/structs?name=" + structName); + builder.addLink("structs", "/structs"); + builder.addLink("program", "/program"); + + sendJsonResponse(exchange, builder.build(), 200); + } catch (TransactionException e) { + Msg.error(this, "Transaction failed: Update Struct Field", e); + sendErrorResponse(exchange, 500, "Failed to update field: " + e.getMessage(), "TRANSACTION_ERROR"); + } catch (Exception e) { + Msg.error(this, "Error updating field", e); + sendErrorResponse(exchange, 400, "Error updating field: " + e.getMessage(), "INVALID_PARAMETER"); + } + } catch (Exception e) { + Msg.error(this, "Unexpected error updating field", e); + sendErrorResponse(exchange, 500, "Error updating field: " + e.getMessage(), "INTERNAL_ERROR"); + } + } + + /** + * Delete a struct data type + * POST /structs/delete + * Required params: name + */ + private void handleDeleteStruct(HttpExchange exchange, Map params) throws IOException { + try { + String structName = params.get("name"); + + if (structName == null || structName.isEmpty()) { + sendErrorResponse(exchange, 400, "Missing required parameter: name", "MISSING_PARAMETERS"); + return; + } + + Program program = getCurrentProgram(); + if (program == null) { + sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED"); + return; + } + + Map resultMap = new HashMap<>(); + resultMap.put("name", structName); + + try { + TransactionHelper.executeInTransaction(program, "Delete Struct", () -> { + DataTypeManager dtm = program.getDataTypeManager(); + + // Find the struct - handle both full paths and simple names + DataType dataType = null; + if (structName.startsWith("/")) { + dataType = dtm.getDataType(structName); + if (dataType == null) { + dataType = dtm.findDataType(structName); + } + } else { + dataType = findStructByName(dtm, structName); + } + + if (dataType == null) { + throw new Exception("Struct not found: " + structName); + } + + if (!(dataType instanceof Structure)) { + throw new Exception("Data type is not a struct: " + structName); + } + + // Store info before deletion + resultMap.put("path", dataType.getPathName()); + resultMap.put("category", dataType.getCategoryPath().getPath()); + + // Remove the struct + dtm.remove(dataType, null); + + return null; + }); + + resultMap.put("message", "Struct deleted successfully"); + + ResponseBuilder builder = new ResponseBuilder(exchange, port) + .success(true) + .result(resultMap); + + builder.addLink("structs", "/structs"); + builder.addLink("program", "/program"); + + sendJsonResponse(exchange, builder.build(), 200); + } catch (TransactionException e) { + Msg.error(this, "Transaction failed: Delete Struct", e); + sendErrorResponse(exchange, 500, "Failed to delete struct: " + e.getMessage(), "TRANSACTION_ERROR"); + } catch (Exception e) { + Msg.error(this, "Error deleting struct", e); + sendErrorResponse(exchange, 400, "Error deleting struct: " + e.getMessage(), "INVALID_PARAMETER"); + } + } catch (Exception e) { + Msg.error(this, "Unexpected error deleting struct", e); + sendErrorResponse(exchange, 500, "Error deleting struct: " + e.getMessage(), "INTERNAL_ERROR"); + } + } + + /** + * Build a detailed information map for a struct including all fields + */ + private Map buildStructInfo(Structure struct) { + Map structInfo = new HashMap<>(); + structInfo.put("name", struct.getName()); + structInfo.put("path", struct.getPathName()); + structInfo.put("size", struct.getLength()); + structInfo.put("category", struct.getCategoryPath().getPath()); + structInfo.put("description", struct.getDescription() != null ? struct.getDescription() : ""); + structInfo.put("numFields", struct.getNumComponents()); + + // Add field details + List> fields = new ArrayList<>(); + for (DataTypeComponent component : struct.getComponents()) { + Map fieldInfo = new HashMap<>(); + fieldInfo.put("name", component.getFieldName() != null ? component.getFieldName() : ""); + fieldInfo.put("offset", component.getOffset()); + fieldInfo.put("length", component.getLength()); + fieldInfo.put("type", component.getDataType().getName()); + fieldInfo.put("typePath", component.getDataType().getPathName()); + fieldInfo.put("comment", component.getComment() != null ? component.getComment() : ""); + fields.add(fieldInfo); + } + structInfo.put("fields", fields); + + return structInfo; + } + + /** + * Find a struct by name, searching through all data types + */ + private DataType findStructByName(DataTypeManager dtm, String structName) { + final DataType[] result = new DataType[1]; + + dtm.getAllDataTypes().forEachRemaining(dt -> { + if (dt instanceof Structure && dt.getName().equals(structName)) { + if (result[0] == null) { + result[0] = dt; + } + } + }); + + return result[0]; + } + + /** + * Find a data type by name, trying multiple lookup methods + */ + private DataType findDataType(DataTypeManager dtm, String typeName) { + // Try direct lookup with path + DataType dataType = dtm.getDataType("/" + typeName); + + // Try without path + if (dataType == null) { + dataType = dtm.findDataType("/" + typeName); + } + + // Try built-in primitive types + if (dataType == null) { + switch(typeName.toLowerCase()) { + case "byte": + dataType = new ByteDataType(); + break; + case "char": + dataType = new CharDataType(); + break; + case "word": + dataType = new WordDataType(); + break; + case "dword": + dataType = new DWordDataType(); + break; + case "qword": + dataType = new QWordDataType(); + break; + case "float": + dataType = new FloatDataType(); + break; + case "double": + dataType = new DoubleDataType(); + break; + case "int": + dataType = new IntegerDataType(); + break; + case "long": + dataType = new LongDataType(); + break; + case "pointer": + dataType = new PointerDataType(); + break; + case "string": + dataType = new StringDataType(); + break; + } + } + + return dataType; + } +}