feat: add struct data type management API

Add endpoints and MCP tools to create, read, update, and delete struct
data types in Ghidra's data type manager. Enables programmatic definition
of complex data structures for reverse engineering workflows.

Includes pagination, category filtering, and field-level operations
(add, update by name or offset).
This commit is contained in:
Teal Bauer 2025-11-14 12:10:34 +01:00
parent 24f5f1698a
commit 30d9bb17da
4 changed files with 1202 additions and 0 deletions

View File

@ -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 ### 7. Memory Segments
Represents memory blocks/sections defined in the program. Represents memory blocks/sections defined in the program.

View File

@ -48,6 +48,7 @@ The API is organized into namespaces for different types of operations:
- instances_* : For managing Ghidra instances - instances_* : For managing Ghidra instances
- functions_* : For working with functions - functions_* : For working with functions
- data_* : For working with data items - data_* : For working with data items
- structs_* : For creating and managing struct data types
- memory_* : For memory access - memory_* : For memory access
- xrefs_* : For cross-references - xrefs_* : For cross-references
- analysis_* : For program analysis - 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) response = safe_post(port, "data/type", payload)
return simplify_response(response) 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 # Analysis tools
@mcp.tool() @mcp.tool()
def analysis_run(port: int = None, analysis_options: dict = None) -> dict: def analysis_run(port: int = None, analysis_options: dict = None) -> dict:

View File

@ -139,6 +139,7 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
new SymbolEndpoints(currentProgram, port, tool).registerEndpoints(server); new SymbolEndpoints(currentProgram, port, tool).registerEndpoints(server);
new NamespaceEndpoints(currentProgram, port, tool).registerEndpoints(server); new NamespaceEndpoints(currentProgram, port, tool).registerEndpoints(server);
new DataEndpoints(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 MemoryEndpoints(currentProgram, port, tool).registerEndpoints(server);
new XrefsEndpoints(currentProgram, port, tool).registerEndpoints(server); new XrefsEndpoints(currentProgram, port, tool).registerEndpoints(server);
new AnalysisEndpoints(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("data", "/data")
.addLink("strings", "/strings") .addLink("strings", "/strings")
.addLink("segments", "/segments") .addLink("segments", "/segments")
.addLink("structs", "/structs")
.addLink("memory", "/memory") .addLink("memory", "/memory")
.addLink("xrefs", "/xrefs") .addLink("xrefs", "/xrefs")
.addLink("analysis", "/analysis") .addLink("analysis", "/analysis")

View File

@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<Map<String, Object>> 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<String, Object> 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<String, Object> links = new HashMap<>();
Map<String, String> 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<Map<String, Object>> 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<String, Object> 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<String, String> 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<String, Object> 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<String, String> 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<String, Object> 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<String, String> 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<String, Object> 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<String, String> 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<String, Object> 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<String, Object> buildStructInfo(Structure struct) {
Map<String, Object> 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<Map<String, Object>> fields = new ArrayList<>();
for (DataTypeComponent component : struct.getComponents()) {
Map<String, Object> 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;
}
}