Merge pull request #11 from starsong-consulting/feat/struct-management

feat: add struct data type management API
This commit is contained in:
Teal Bauer 2025-11-14 12:16:53 +01:00 committed by GitHub
commit 8268e55a08
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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
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
- 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:

View File

@ -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")

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