diff --git a/pom.xml b/pom.xml index 7411225..f6c38a8 100644 --- a/pom.xml +++ b/pom.xml @@ -69,6 +69,12 @@ ${project.basedir}/lib/Gui.jar + + org.reflections + reflections + 0.10.2 + + junit @@ -85,6 +91,12 @@ + + 21 + 21 + UTF-8 + + @@ -93,13 +105,37 @@ maven-compiler-plugin 3.13.0 - 8 - 8 + 21 + 21 -Xlint:-options + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.0 + + + package + + shade + + + GhidraMCP + + + + + ghidra:* + + + + + + diff --git a/src/main/java/com/lauriewired/GhidraMCPPlugin.java b/src/main/java/com/lauriewired/GhidraMCPPlugin.java index c90614b..b29e353 100644 --- a/src/main/java/com/lauriewired/GhidraMCPPlugin.java +++ b/src/main/java/com/lauriewired/GhidraMCPPlugin.java @@ -1,77 +1,21 @@ package com.lauriewired; +import com.lauriewired.handlers.Handler; import ghidra.framework.plugintool.Plugin; import ghidra.framework.plugintool.PluginTool; -import ghidra.program.model.address.Address; -import ghidra.program.model.address.GlobalNamespace; -import ghidra.program.model.listing.*; -import ghidra.program.model.mem.MemoryBlock; -import ghidra.program.model.symbol.*; -import ghidra.program.model.symbol.ReferenceManager; -import ghidra.program.model.symbol.ReferenceIterator; -import ghidra.program.model.symbol.RefType; -import ghidra.program.model.pcode.HighFunction; -import ghidra.program.model.pcode.HighSymbol; -import ghidra.program.model.pcode.LocalSymbolMap; -import ghidra.program.model.pcode.HighFunctionDBUtil; -import ghidra.program.model.pcode.HighFunctionDBUtil.ReturnCommitOption; -import ghidra.app.decompiler.DecompInterface; -import ghidra.app.decompiler.DecompileOptions; -import ghidra.app.decompiler.DecompileResults; import ghidra.app.plugin.PluginCategoryNames; -import ghidra.app.services.CodeViewerService; -import ghidra.app.services.ProgramManager; -import ghidra.app.util.PseudoDisassembler; -import ghidra.app.cmd.function.SetVariableNameCmd; -import ghidra.program.model.symbol.SourceType; -import ghidra.util.exception.DuplicateNameException; -import ghidra.util.exception.InvalidInputException; import ghidra.framework.plugintool.PluginInfo; import ghidra.framework.plugintool.util.PluginStatus; -import ghidra.program.util.ProgramLocation; import ghidra.util.Msg; -import ghidra.util.task.ConsoleTaskMonitor; -import ghidra.util.task.TaskMonitor; -import ghidra.util.task.TaskMonitorAdapter; -import ghidra.program.model.pcode.HighVariable; -import ghidra.program.model.pcode.Varnode; -import ghidra.program.model.data.DataType; -import ghidra.program.model.data.DataTypeConflictHandler; -import ghidra.program.model.data.DataTypeComponent; -import ghidra.program.model.data.DataTypeManager; -import ghidra.program.model.data.Structure; -import ghidra.program.model.data.StructureDataType; -import ghidra.program.model.data.PointerDataType; -import ghidra.program.model.data.Undefined1DataType; -import ghidra.program.model.listing.Variable; -import ghidra.program.model.mem.Memory; -import ghidra.app.decompiler.component.DecompilerUtils; -import ghidra.app.decompiler.ClangToken; import ghidra.framework.options.Options; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; -import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpServer; +import org.reflections.Reflections; -import javax.swing.SwingUtilities; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Constructor; import java.net.InetSocketAddress; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; import java.util.*; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -import ghidra.program.model.data.CategoryPath; -import ghidra.app.services.DataTypeManagerService; -import ghidra.util.data.DataTypeParser; -import ghidra.util.data.DataTypeParser.AllowedDataTypes; /** * GhidraMCP Plugin - Model Context Protocol Server for Ghidra @@ -113,12 +57,12 @@ * to ensure thread safety with Ghidra's event dispatch thread. * * @author LaurieWired - * @version 1.3.2 + * @version 2.0 * @since Ghidra 11.3.2 * @see ghidra.framework.plugintool.Plugin * @see com.sun.net.httpserver.HttpServer */ -@PluginInfo(status = PluginStatus.RELEASED, packageName = ghidra.app.DeveloperPluginPackage.NAME, category = "Developer", shortDescription = "HTTP server plugin", description = "Starts an embedded HTTP server to expose program data. Port configurable via Tool Options.") +@PluginInfo(status = PluginStatus.RELEASED, packageName = ghidra.app.DeveloperPluginPackage.NAME, category = PluginCategoryNames.ANALYSIS, shortDescription = "HTTP server plugin", description = "Starts an embedded HTTP server to expose program data. Port configurable via Tool Options.") public class GhidraMCPPlugin extends Plugin { /** The embedded HTTP server instance that handles all API requests */ @@ -145,6 +89,10 @@ public class GhidraMCPPlugin extends Plugin { /** Default decompile timeout in seconds */ private static final int DEFAULT_DECOMPILE_TIMEOUT = 30; + /** HashMap to store all registered API routes */ + private static final HashMap routes = new HashMap<>(); + + /** The timeout for decompilation requests in seconds */ private int decompileTimeout; /** @@ -261,311 +209,32 @@ private void startServer() throws IOException { server = HttpServer.create(inetAddress, 0); - // Each listing endpoint uses offset & limit from query params: - server.createContext("/methods", exchange -> { - Map qparams = parseQueryParams(exchange); - int offset = parseIntOrDefault(qparams.get("offset"), 0); - int limit = parseIntOrDefault(qparams.get("limit"), 100); - sendResponse(exchange, getAllFunctionNames(offset, limit)); - }); - - server.createContext("/classes", exchange -> { - Map qparams = parseQueryParams(exchange); - int offset = parseIntOrDefault(qparams.get("offset"), 0); - int limit = parseIntOrDefault(qparams.get("limit"), 100); - sendResponse(exchange, getAllClassNames(offset, limit)); - }); - - server.createContext("/decompile", exchange -> { - String name = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); - sendResponse(exchange, decompileFunctionByName(name)); - }); - - server.createContext("/renameFunction", exchange -> { - Map params = parsePostParams(exchange); - String response = renameFunction(params.get("oldName"), params.get("newName")) - ? "Renamed successfully" - : "Rename failed"; - sendResponse(exchange, response); - }); - - server.createContext("/renameData", exchange -> { - Map params = parsePostParams(exchange); - renameDataAtAddress(params.get("address"), params.get("newName")); - sendResponse(exchange, "Rename data attempted"); - }); - - server.createContext("/renameVariable", exchange -> { - Map params = parsePostParams(exchange); - String functionName = params.get("functionName"); - String oldName = params.get("oldName"); - String newName = params.get("newName"); - String result = renameVariableInFunction(functionName, oldName, newName); - sendResponse(exchange, result); - }); - - server.createContext("/segments", exchange -> { - Map qparams = parseQueryParams(exchange); - int offset = parseIntOrDefault(qparams.get("offset"), 0); - int limit = parseIntOrDefault(qparams.get("limit"), 100); - sendResponse(exchange, listSegments(offset, limit)); - }); - - server.createContext("/imports", exchange -> { - Map qparams = parseQueryParams(exchange); - int offset = parseIntOrDefault(qparams.get("offset"), 0); - int limit = parseIntOrDefault(qparams.get("limit"), 100); - sendResponse(exchange, listImports(offset, limit)); - }); - - server.createContext("/exports", exchange -> { - Map qparams = parseQueryParams(exchange); - int offset = parseIntOrDefault(qparams.get("offset"), 0); - int limit = parseIntOrDefault(qparams.get("limit"), 100); - sendResponse(exchange, listExports(offset, limit)); - }); - - server.createContext("/namespaces", exchange -> { - Map qparams = parseQueryParams(exchange); - int offset = parseIntOrDefault(qparams.get("offset"), 0); - int limit = parseIntOrDefault(qparams.get("limit"), 100); - sendResponse(exchange, listNamespaces(offset, limit)); - }); - - server.createContext("/data", exchange -> { - Map qparams = parseQueryParams(exchange); - int offset = parseIntOrDefault(qparams.get("offset"), 0); - int limit = parseIntOrDefault(qparams.get("limit"), 100); - sendResponse(exchange, listDefinedData(offset, limit)); - }); - - server.createContext("/searchFunctions", exchange -> { - Map qparams = parseQueryParams(exchange); - String searchTerm = qparams.get("query"); - int offset = parseIntOrDefault(qparams.get("offset"), 0); - int limit = parseIntOrDefault(qparams.get("limit"), 100); - sendResponse(exchange, searchFunctionsByName(searchTerm, offset, limit)); - }); - - // New API endpoints based on requirements - - server.createContext("/get_function_by_address", exchange -> { - Map qparams = parseQueryParams(exchange); - String address = qparams.get("address"); - sendResponse(exchange, getFunctionByAddress(address)); - }); - - server.createContext("/get_current_address", exchange -> { - sendResponse(exchange, getCurrentAddress()); - }); - - server.createContext("/get_current_function", exchange -> { - sendResponse(exchange, getCurrentFunction()); - }); - - server.createContext("/list_functions", exchange -> { - sendResponse(exchange, listFunctions()); - }); - - server.createContext("/decompile_function", exchange -> { - Map qparams = parseQueryParams(exchange); - String address = qparams.get("address"); - sendResponse(exchange, decompileFunctionByAddress(address)); - }); - - server.createContext("/disassemble_function", exchange -> { - Map qparams = parseQueryParams(exchange); - String address = qparams.get("address"); - sendResponse(exchange, disassembleFunction(address)); - }); - - server.createContext("/set_decompiler_comment", exchange -> { - Map params = parsePostParams(exchange); - String address = params.get("address"); - String comment = params.get("comment"); - boolean success = setDecompilerComment(address, comment); - sendResponse(exchange, success ? "Comment set successfully" : "Failed to set comment"); - }); - - server.createContext("/set_disassembly_comment", exchange -> { - Map params = parsePostParams(exchange); - String address = params.get("address"); - String comment = params.get("comment"); - boolean success = setDisassemblyComment(address, comment); - sendResponse(exchange, success ? "Comment set successfully" : "Failed to set comment"); - }); - - server.createContext("/rename_function_by_address", exchange -> { - Map params = parsePostParams(exchange); - String functionAddress = params.get("function_address"); - String newName = params.get("new_name"); - boolean success = renameFunctionByAddress(functionAddress, newName); - sendResponse(exchange, success ? "Function renamed successfully" : "Failed to rename function"); - }); - - server.createContext("/set_function_prototype", exchange -> { - Map params = parsePostParams(exchange); - String functionAddress = params.get("function_address"); - String prototype = params.get("prototype"); - - // Call the set prototype function and get detailed result - PrototypeResult result = setFunctionPrototype(functionAddress, prototype); - - if (result.isSuccess()) { - // Even with successful operations, include any warning messages for debugging - String successMsg = "Function prototype set successfully"; - if (!result.getErrorMessage().isEmpty()) { - successMsg += "\n\nWarnings/Debug Info:\n" + result.getErrorMessage(); + Reflections reflections = new Reflections("com.lauriewired.handlers"); + Set> subclasses = reflections.getSubTypesOf(Handler.class); + for (Class clazz : subclasses) { + System.out.println(clazz.getName()); + try { + Constructor constructor = clazz.getConstructor(PluginTool.class); + Handler handler = (Handler) constructor.newInstance(tool); + if (routes.containsKey(handler.getPath())) { + Msg.error(this, "Handler class " + clazz.getName() + " already registered, skipped."); + continue; } - sendResponse(exchange, successMsg); - } else { - // Return the detailed error message to the client - sendResponse(exchange, "Failed to set function prototype: " + result.getErrorMessage()); - } - }); - - server.createContext("/set_local_variable_type", exchange -> { - Map params = parsePostParams(exchange); - String functionAddress = params.get("function_address"); - String variableName = params.get("variable_name"); - String newType = params.get("new_type"); - - // Capture detailed information about setting the type - StringBuilder responseMsg = new StringBuilder(); - responseMsg.append("Setting variable type: ").append(variableName) - .append(" to ").append(newType) - .append(" in function at ").append(functionAddress).append("\n\n"); - - // Attempt to find the data type in various categories - Program program = getCurrentProgram(); - if (program != null) { - DataTypeManager dtm = program.getDataTypeManager(); - DataType directType = findDataTypeByNameInAllCategories(dtm, newType); - if (directType != null) { - responseMsg.append("Found type: ").append(directType.getPathName()).append("\n"); - } else if (newType.startsWith("P") && newType.length() > 1) { - String baseTypeName = newType.substring(1); - DataType baseType = findDataTypeByNameInAllCategories(dtm, baseTypeName); - if (baseType != null) { - responseMsg.append("Found base type for pointer: ").append(baseType.getPathName()).append("\n"); - } else { - responseMsg.append("Base type not found for pointer: ").append(baseTypeName).append("\n"); + routes.put(handler.getPath(), handler); + server.createContext(handler.getPath(), exchange -> { + try { + handler.handle(exchange); + } catch (Exception e) { + throw new RuntimeException(e); } - } else { - responseMsg.append("Type not found directly: ").append(newType).append("\n"); - } - } - - // Try to set the type - boolean success = setLocalVariableType(functionAddress, variableName, newType); - - String successMsg = success ? "Variable type set successfully" : "Failed to set variable type"; - responseMsg.append("\nResult: ").append(successMsg); - - sendResponse(exchange, responseMsg.toString()); - }); - - server.createContext("/xrefs_to", exchange -> { - Map qparams = parseQueryParams(exchange); - String address = qparams.get("address"); - int offset = parseIntOrDefault(qparams.get("offset"), 0); - int limit = parseIntOrDefault(qparams.get("limit"), 100); - sendResponse(exchange, getXrefsTo(address, offset, limit)); - }); - - server.createContext("/xrefs_from", exchange -> { - Map qparams = parseQueryParams(exchange); - String address = qparams.get("address"); - int offset = parseIntOrDefault(qparams.get("offset"), 0); - int limit = parseIntOrDefault(qparams.get("limit"), 100); - sendResponse(exchange, getXrefsFrom(address, offset, limit)); - }); - - server.createContext("/function_xrefs", exchange -> { - Map qparams = parseQueryParams(exchange); - String name = qparams.get("name"); - int offset = parseIntOrDefault(qparams.get("offset"), 0); - int limit = parseIntOrDefault(qparams.get("limit"), 100); - sendResponse(exchange, getFunctionXrefs(name, offset, limit)); - }); - - server.createContext("/strings", exchange -> { - Map qparams = parseQueryParams(exchange); - int offset = parseIntOrDefault(qparams.get("offset"), 0); - int limit = parseIntOrDefault(qparams.get("limit"), 100); - String filter = qparams.get("filter"); - sendResponse(exchange, listDefinedStrings(offset, limit, filter)); - }); - - server.createContext("/create_struct", exchange -> { - Map params = parsePostParams(exchange); - String name = params.get("name"); - String category = params.get("category"); - long size = parseIntOrDefault(params.get("size"), 0); - String membersJson = params.get("members"); // Optional - - if (name == null || name.isEmpty()) { - sendResponse(exchange, "Struct name is required"); - return; - } - sendResponse(exchange, createStruct(name, category, (int) size, membersJson)); - }); - - server.createContext("/add_struct_members", exchange -> { - Map params = parsePostParams(exchange); - String structName = params.get("struct_name"); - String category = params.get("category"); - String membersJson = params.get("members"); - - if (structName == null || membersJson == null) { - sendResponse(exchange, "struct_name and members are required"); - return; - } - sendResponse(exchange, addStructMembers(structName, category, membersJson)); - }); - - server.createContext("/clear_struct", exchange -> { - Map params = parsePostParams(exchange); - String structName = params.get("struct_name"); - String category = params.get("category"); - if (structName == null) { - sendResponse(exchange, "struct_name is required"); - return; - } - sendResponse(exchange, clearStruct(structName, category)); - }); - - server.createContext("/get_struct", exchange -> { - Map qparams = parseQueryParams(exchange); - String structName = qparams.get("name"); - String category = qparams.get("category"); - if (structName == null) { - sendResponse(exchange, "name is required"); - return; + }); + } catch (NoSuchMethodException e) { + Msg.error(this, "Handler class " + clazz.getName() + + " doesn't have constructor xxx(PluginTool tool), skipped."); + } catch (Exception e) { + e.printStackTrace(); } - sendResponse(exchange, getStruct(structName, category)); - }); - - server.createContext("/get_data_by_label", exchange -> { - Map q = parseQueryParams(exchange); - String label = q.get("label"); - sendResponse(exchange, getDataByLabel(label)); - }); - - server.createContext("/get_bytes", exchange -> { - Map q = parseQueryParams(exchange); - String addrStr = q.get("address"); - int size = parseIntOrDefault(q.get("size"), 1); - sendResponse(exchange, getBytes(addrStr, size)); - }); - - server.createContext("/search_bytes", exchange -> { - Map q = parseQueryParams(exchange); - String bytesHex = q.get("bytes"); - int offset = parseIntOrDefault(q.get("offset"), 0); - int limit = parseIntOrDefault(q.get("limit"), 100); - sendResponse(exchange, searchBytes(bytesHex, offset, limit)); - }); + } server.setExecutor(null); new Thread(() -> { @@ -579,1773 +248,6 @@ private void startServer() throws IOException { }, "GhidraMCP-HTTP-Server").start(); } - // ---------------------------------------------------------------------------------- - // Pagination-aware listing methods - // ---------------------------------------------------------------------------------- - - /** - * Retrieves a paginated list of all function names in the current program. - * - * This method scans the program's function manager to collect all function - * names - * and returns a subset based on the specified offset and limit parameters. - * The results are returned as a newline-separated string. - * - * @param offset Starting index for pagination (0-based) - * @param limit Maximum number of function names to return - * @return Newline-separated string of function names, or error message if no - * program loaded - * @see #paginateList(List, int, int) - * @see FunctionManager#getFunctions(boolean) - */ - private String getAllFunctionNames(int offset, int limit) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - - List names = new ArrayList<>(); - for (Function f : program.getFunctionManager().getFunctions(true)) { - names.add(f.getName()); - } - return paginateList(names, offset, limit); - } - - private String getAllClassNames(int offset, int limit) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - - Set classNames = new HashSet<>(); - for (Symbol symbol : program.getSymbolTable().getAllSymbols(true)) { - Namespace ns = symbol.getParentNamespace(); - if (ns != null && !ns.isGlobal()) { - classNames.add(ns.getName()); - } - } - // Convert set to list for pagination - List sorted = new ArrayList<>(classNames); - Collections.sort(sorted); - return paginateList(sorted, offset, limit); - } - - private String listSegments(int offset, int limit) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - - List lines = new ArrayList<>(); - for (MemoryBlock block : program.getMemory().getBlocks()) { - lines.add(String.format("%s: %s - %s", block.getName(), block.getStart(), block.getEnd())); - } - return paginateList(lines, offset, limit); - } - - private String listImports(int offset, int limit) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - - List lines = new ArrayList<>(); - for (Symbol symbol : program.getSymbolTable().getExternalSymbols()) { - lines.add(symbol.getName() + " -> " + symbol.getAddress()); - } - return paginateList(lines, offset, limit); - } - - private String listExports(int offset, int limit) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - - SymbolTable table = program.getSymbolTable(); - SymbolIterator it = table.getAllSymbols(true); - - List lines = new ArrayList<>(); - while (it.hasNext()) { - Symbol s = it.next(); - // On older Ghidra, "export" is recognized via isExternalEntryPoint() - if (s.isExternalEntryPoint()) { - lines.add(s.getName() + " -> " + s.getAddress()); - } - } - return paginateList(lines, offset, limit); - } - - private String listNamespaces(int offset, int limit) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - - Set namespaces = new HashSet<>(); - for (Symbol symbol : program.getSymbolTable().getAllSymbols(true)) { - Namespace ns = symbol.getParentNamespace(); - if (ns != null && !(ns instanceof GlobalNamespace)) { - namespaces.add(ns.getName()); - } - } - List sorted = new ArrayList<>(namespaces); - Collections.sort(sorted); - return paginateList(sorted, offset, limit); - } - - private String listDefinedData(int offset, int limit) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - - List lines = new ArrayList<>(); - for (MemoryBlock block : program.getMemory().getBlocks()) { - DataIterator it = program.getListing().getDefinedData(block.getStart(), true); - while (it.hasNext()) { - Data data = it.next(); - if (block.contains(data.getAddress())) { - String label = data.getLabel() != null ? data.getLabel() : "(unnamed)"; - String valRepr = data.getDefaultValueRepresentation(); - lines.add(String.format("%s: %s = %s", - data.getAddress(), - escapeNonAscii(label), - escapeNonAscii(valRepr))); - } - } - } - return paginateList(lines, offset, limit); - } - - private String searchFunctionsByName(String searchTerm, int offset, int limit) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - if (searchTerm == null || searchTerm.isEmpty()) - return "Search term is required"; - - List matches = new ArrayList<>(); - for (Function func : program.getFunctionManager().getFunctions(true)) { - String name = func.getName(); - // simple substring match - if (name.toLowerCase().contains(searchTerm.toLowerCase())) { - matches.add(String.format("%s @ %s", name, func.getEntryPoint())); - } - } - - Collections.sort(matches); - - if (matches.isEmpty()) { - return "No functions matching '" + searchTerm + "'"; - } - return paginateList(matches, offset, limit); - } - - // ---------------------------------------------------------------------------------- - // Logic for rename, decompile, etc. - // ---------------------------------------------------------------------------------- - - /** - * Decompiles a function by its name and returns the generated C pseudocode. - * - * This method locates a function by name within the current program and uses - * Ghidra's DecompInterface to generate readable C-like pseudocode. The - * decompilation - * process includes: - *
    - *
  • Function signature analysis
  • - *
  • Local variable identification and typing
  • - *
  • Control flow reconstruction
  • - *
  • High-level language construct generation
  • - *
- * - * Thread Safety: This method uses Ghidra's decompiler interface which - * is thread-safe for read operations. - * - * @param name The exact name of the function to decompile (case-sensitive) - * @return The decompiled C pseudocode as a string, or an error message if: - *
    - *
  • No program is currently loaded
  • - *
  • Function with the specified name is not found
  • - *
  • Decompilation fails due to analysis issues
  • - *
- * @see DecompInterface#decompileFunction(Function, int, TaskMonitor) - * @see #decompileFunctionByAddress(String) - */ - private String decompileFunctionByName(String name) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - DecompInterface decomp = new DecompInterface(); - DecompileOptions options = new DecompileOptions(); - options.setRespectReadOnly(true); - decomp.setOptions(options); - decomp.openProgram(program); - for (Function func : program.getFunctionManager().getFunctions(true)) { - if (func.getName().equals(name)) { - DecompileResults result = decomp.decompileFunction(func, this.decompileTimeout, - new ConsoleTaskMonitor()); - if (result != null && result.decompileCompleted()) { - return result.getDecompiledFunction().getC(); - } else { - return "Decompilation failed"; - } - } - } - return "Function not found"; - } - - /** - * Renames a function from its current name to a new user-specified name. - * - * This method performs a thread-safe rename operation by: - *
    - *
  1. Locating the function by its current name
  2. - *
  3. Executing the rename within a Ghidra transaction on the Swing EDT
  4. - *
  5. Marking the new name as USER_DEFINED source type
  6. - *
  7. Committing or rolling back the transaction based on success
  8. - *
- * - * Thread Safety: This method uses SwingUtilities.invokeAndWait() to - * ensure - * all Ghidra API calls occur on the Event Dispatch Thread, preventing race - * conditions - * and maintaining data integrity. - * - * Transaction Management: The rename operation is wrapped in a Ghidra - * transaction - * to ensure atomicity - either the rename succeeds completely or is fully - * reverted. - * - * @param oldName The current name of the function to rename (case-sensitive) - * @param newName The desired new name for the function - * @return {@code true} if the function was successfully renamed, - * {@code false} if no program is loaded, function not found, or rename - * failed - * @throws IllegalArgumentException if either parameter is null or empty - * @see Function#setName(String, SourceType) - * @see SourceType#USER_DEFINED - * @see Program#startTransaction(String) - */ - private boolean renameFunction(String oldName, String newName) { - Program program = getCurrentProgram(); - if (program == null) - return false; - - AtomicBoolean successFlag = new AtomicBoolean(false); - try { - SwingUtilities.invokeAndWait(() -> { - int tx = program.startTransaction("Rename function via HTTP"); - try { - for (Function func : program.getFunctionManager().getFunctions(true)) { - if (func.getName().equals(oldName)) { - func.setName(newName, SourceType.USER_DEFINED); - successFlag.set(true); - break; - } - } - } catch (Exception e) { - Msg.error(this, "Error renaming function", e); - } finally { - successFlag.set(program.endTransaction(tx, successFlag.get())); - } - }); - } catch (InterruptedException | InvocationTargetException e) { - Msg.error(this, "Failed to execute rename on Swing thread", e); - } - return successFlag.get(); - } - - private void renameDataAtAddress(String addressStr, String newName) { - Program program = getCurrentProgram(); - if (program == null) - return; - - try { - SwingUtilities.invokeAndWait(() -> { - int tx = program.startTransaction("Rename data"); - try { - Address addr = program.getAddressFactory().getAddress(addressStr); - Listing listing = program.getListing(); - Data data = listing.getDefinedDataAt(addr); - if (data != null) { - SymbolTable symTable = program.getSymbolTable(); - Symbol symbol = symTable.getPrimarySymbol(addr); - if (symbol != null) { - symbol.setName(newName, SourceType.USER_DEFINED); - } else { - symTable.createLabel(addr, newName, SourceType.USER_DEFINED); - } - } - } catch (Exception e) { - Msg.error(this, "Rename data error", e); - } finally { - program.endTransaction(tx, true); - } - }); - } catch (InterruptedException | InvocationTargetException e) { - Msg.error(this, "Failed to execute rename data on Swing thread", e); - } - } - - private String renameVariableInFunction(String functionName, String oldVarName, String newVarName) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - - DecompInterface decomp = new DecompInterface(); - decomp.openProgram(program); - - Function func = null; - for (Function f : program.getFunctionManager().getFunctions(true)) { - if (f.getName().equals(functionName)) { - func = f; - break; - } - } - - if (func == null) { - return "Function not found"; - } - - DecompileResults result = decomp.decompileFunction(func, this.decompileTimeout, new ConsoleTaskMonitor()); - if (result == null || !result.decompileCompleted()) { - return "Decompilation failed"; - } - - HighFunction highFunction = result.getHighFunction(); - if (highFunction == null) { - return "Decompilation failed (no high function)"; - } - - LocalSymbolMap localSymbolMap = highFunction.getLocalSymbolMap(); - if (localSymbolMap == null) { - return "Decompilation failed (no local symbol map)"; - } - - HighSymbol highSymbol = null; - Iterator symbols = localSymbolMap.getSymbols(); - while (symbols.hasNext()) { - HighSymbol symbol = symbols.next(); - String symbolName = symbol.getName(); - - if (symbolName.equals(oldVarName)) { - highSymbol = symbol; - } - if (symbolName.equals(newVarName)) { - return "Error: A variable with name '" + newVarName + "' already exists in this function"; - } - } - - if (highSymbol == null) { - return "Variable not found"; - } - - boolean commitRequired = checkFullCommit(highSymbol, highFunction); - - final HighSymbol finalHighSymbol = highSymbol; - final Function finalFunction = func; - AtomicBoolean successFlag = new AtomicBoolean(false); - - try { - SwingUtilities.invokeAndWait(() -> { - int tx = program.startTransaction("Rename variable"); - try { - if (commitRequired) { - HighFunctionDBUtil.commitParamsToDatabase(highFunction, false, - ReturnCommitOption.NO_COMMIT, finalFunction.getSignatureSource()); - } - HighFunctionDBUtil.updateDBVariable( - finalHighSymbol, - newVarName, - null, - SourceType.USER_DEFINED); - successFlag.set(true); - } catch (Exception e) { - Msg.error(this, "Failed to rename variable", e); - } finally { - successFlag.set(program.endTransaction(tx, true)); - } - }); - } catch (InterruptedException | InvocationTargetException e) { - String errorMsg = "Failed to execute rename on Swing thread: " + e.getMessage(); - Msg.error(this, errorMsg, e); - return errorMsg; - } - return successFlag.get() ? "Variable renamed" : "Failed to rename variable"; - } - - /** - * Copied from AbstractDecompilerAction.checkFullCommit, it's protected. - * Compare the given HighFunction's idea of the prototype with the Function's - * idea. - * Return true if there is a difference. If a specific symbol is being changed, - * it can be passed in to check whether or not the prototype is being affected. - * - * @param highSymbol (if not null) is the symbol being modified - * @param hfunction is the given HighFunction - * @return true if there is a difference (and a full commit is required) - */ - protected static boolean checkFullCommit(HighSymbol highSymbol, HighFunction hfunction) { - if (highSymbol != null && !highSymbol.isParameter()) { - return false; - } - Function function = hfunction.getFunction(); - Parameter[] parameters = function.getParameters(); - LocalSymbolMap localSymbolMap = hfunction.getLocalSymbolMap(); - int numParams = localSymbolMap.getNumParams(); - if (numParams != parameters.length) { - return true; - } - - for (int i = 0; i < numParams; i++) { - HighSymbol param = localSymbolMap.getParamSymbol(i); - if (param.getCategoryIndex() != i) { - return true; - } - VariableStorage storage = param.getStorage(); - // Don't compare using the equals method so that DynamicVariableStorage can - // match - if (0 != storage.compareTo(parameters[i].getVariableStorage())) { - return true; - } - } - - return false; - } - - // ---------------------------------------------------------------------------------- - // New methods to implement the new functionalities - // ---------------------------------------------------------------------------------- - - /** - * Get function by address - */ - private String getFunctionByAddress(String addressStr) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - if (addressStr == null || addressStr.isEmpty()) - return "Address is required"; - - try { - Address addr = program.getAddressFactory().getAddress(addressStr); - Function func = program.getFunctionManager().getFunctionAt(addr); - - if (func == null) - return "No function found at address " + addressStr; - - return String.format("Function: %s at %s\nSignature: %s\nEntry: %s\nBody: %s - %s", - func.getName(), - func.getEntryPoint(), - func.getSignature(), - func.getEntryPoint(), - func.getBody().getMinAddress(), - func.getBody().getMaxAddress()); - } catch (Exception e) { - return "Error getting function: " + e.getMessage(); - } - } - - /** - * Get current address selected in Ghidra GUI - */ - private String getCurrentAddress() { - CodeViewerService service = tool.getService(CodeViewerService.class); - if (service == null) - return "Code viewer service not available"; - - ProgramLocation location = service.getCurrentLocation(); - return (location != null) ? location.getAddress().toString() : "No current location"; - } - - /** - * Get current function selected in Ghidra GUI - */ - private String getCurrentFunction() { - CodeViewerService service = tool.getService(CodeViewerService.class); - if (service == null) - return "Code viewer service not available"; - - ProgramLocation location = service.getCurrentLocation(); - if (location == null) - return "No current location"; - - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - - Function func = program.getFunctionManager().getFunctionContaining(location.getAddress()); - if (func == null) - return "No function at current location: " + location.getAddress(); - - return String.format("Function: %s at %s\nSignature: %s", - func.getName(), - func.getEntryPoint(), - func.getSignature()); - } - - /** - * List all functions in the database - */ - private String listFunctions() { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - - StringBuilder result = new StringBuilder(); - for (Function func : program.getFunctionManager().getFunctions(true)) { - result.append(String.format("%s at %s\n", - func.getName(), - func.getEntryPoint())); - } - - return result.toString(); - } - - /** - * Gets a function at the given address or containing the address - * - * @return the function or null if not found - */ - private Function getFunctionForAddress(Program program, Address addr) { - Function func = program.getFunctionManager().getFunctionAt(addr); - if (func == null) { - func = program.getFunctionManager().getFunctionContaining(addr); - } - return func; - } - - /** - * Decompile a function at the given address - */ - private String decompileFunctionByAddress(String addressStr) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - if (addressStr == null || addressStr.isEmpty()) - return "Address is required"; - - try { - Address addr = program.getAddressFactory().getAddress(addressStr); - Function func = getFunctionForAddress(program, addr); - if (func == null) - return "No function found at or containing address " + addressStr; - - DecompInterface decomp = new DecompInterface(); - decomp.openProgram(program); - DecompileResults result = decomp.decompileFunction(func, this.decompileTimeout, new ConsoleTaskMonitor()); - - return (result != null && result.decompileCompleted()) - ? result.getDecompiledFunction().getC() - : "Decompilation failed"; - } catch (Exception e) { - return "Error decompiling function: " + e.getMessage(); - } - } - - /** - * Get assembly code for a function - */ - private String disassembleFunction(String addressStr) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - if (addressStr == null || addressStr.isEmpty()) - return "Address is required"; - - try { - Address addr = program.getAddressFactory().getAddress(addressStr); - Function func = getFunctionForAddress(program, addr); - if (func == null) - return "No function found at or containing address " + addressStr; - - StringBuilder result = new StringBuilder(); - Listing listing = program.getListing(); - Address start = func.getEntryPoint(); - Address end = func.getBody().getMaxAddress(); - - InstructionIterator instructions = listing.getInstructions(start, true); - while (instructions.hasNext()) { - Instruction instr = instructions.next(); - if (instr.getAddress().compareTo(end) > 0) { - break; // Stop if we've gone past the end of the function - } - String comment = listing.getComment(CodeUnit.EOL_COMMENT, instr.getAddress()); - comment = (comment != null) ? "; " + comment : ""; - - result.append(String.format("%s: %s %s\n", - instr.getAddress(), - instr.toString(), - comment)); - } - - return result.toString(); - } catch (Exception e) { - return "Error disassembling function: " + e.getMessage(); - } - } - - /** - * Set a comment using the specified comment type (PRE_COMMENT or EOL_COMMENT) - */ - private boolean setCommentAtAddress(String addressStr, String comment, int commentType, String transactionName) { - Program program = getCurrentProgram(); - if (program == null) - return false; - if (addressStr == null || addressStr.isEmpty() || comment == null) - return false; - - AtomicBoolean success = new AtomicBoolean(false); - - try { - SwingUtilities.invokeAndWait(() -> { - int tx = program.startTransaction(transactionName); - try { - Address addr = program.getAddressFactory().getAddress(addressStr); - program.getListing().setComment(addr, commentType, comment); - success.set(true); - } catch (Exception e) { - Msg.error(this, "Error setting " + transactionName.toLowerCase(), e); - } finally { - success.set(program.endTransaction(tx, success.get())); - } - }); - } catch (InterruptedException | InvocationTargetException e) { - Msg.error(this, "Failed to execute " + transactionName.toLowerCase() + " on Swing thread", e); - } - - return success.get(); - } - - /** - * Set a comment for a given address in the function pseudocode - */ - private boolean setDecompilerComment(String addressStr, String comment) { - return setCommentAtAddress(addressStr, comment, CodeUnit.PRE_COMMENT, "Set decompiler comment"); - } - - /** - * Set a comment for a given address in the function disassembly - */ - private boolean setDisassemblyComment(String addressStr, String comment) { - return setCommentAtAddress(addressStr, comment, CodeUnit.EOL_COMMENT, "Set disassembly comment"); - } - - /** - * Result container for function prototype setting operations. - * - * This immutable class encapsulates the outcome of attempting to set a - * function's - * prototype signature, including both success/failure status and detailed error - * information for debugging purposes. - * - *

- * The class is designed to provide comprehensive feedback about prototype - * setting operations, which can be complex due to: - *

    - *
  • C signature parsing requirements
  • - *
  • Data type resolution in various categories
  • - *
  • Parameter count and type validation
  • - *
  • Ghidra's internal function signature constraints
  • - *
- * - * Usage Example: - * - *
{@code
-	 * PrototypeResult result = setFunctionPrototype("0x401000", "int foo(char* str, int len)");
-	 * if (result.isSuccess()) {
-	 * 	System.out.println("Prototype set successfully");
-	 * } else {
-	 * 	System.err.println("Failed: " + result.getErrorMessage());
-	 * }
-	 * }
- * - * @see #setFunctionPrototype(String, String) - * @since 1.3.0 - */ - private static class PrototypeResult { - private final boolean success; - private final String errorMessage; - - public PrototypeResult(boolean success, String errorMessage) { - this.success = success; - this.errorMessage = errorMessage; - } - - public boolean isSuccess() { - return success; - } - - public String getErrorMessage() { - return errorMessage; - } - } - - /** - * Rename a function by its address - */ - private boolean renameFunctionByAddress(String functionAddrStr, String newName) { - Program program = getCurrentProgram(); - if (program == null) - return false; - if (functionAddrStr == null || functionAddrStr.isEmpty() || - newName == null || newName.isEmpty()) { - return false; - } - - AtomicBoolean success = new AtomicBoolean(false); - - try { - SwingUtilities.invokeAndWait(() -> { - performFunctionRename(program, functionAddrStr, newName, success); - }); - } catch (InterruptedException | InvocationTargetException e) { - Msg.error(this, "Failed to execute rename function on Swing thread", e); - } - - return success.get(); - } - - /** - * Helper method to perform the actual function rename within a transaction - */ - private void performFunctionRename(Program program, String functionAddrStr, String newName, AtomicBoolean success) { - int tx = program.startTransaction("Rename function by address"); - try { - Address addr = program.getAddressFactory().getAddress(functionAddrStr); - Function func = getFunctionForAddress(program, addr); - - if (func == null) { - Msg.error(this, "Could not find function at address: " + functionAddrStr); - return; - } - - func.setName(newName, SourceType.USER_DEFINED); - success.set(true); - } catch (Exception e) { - Msg.error(this, "Error renaming function by address", e); - } finally { - program.endTransaction(tx, success.get()); - } - } - - /** - * Set a function's prototype with proper error handling using - * ApplyFunctionSignatureCmd - */ - private PrototypeResult setFunctionPrototype(String functionAddrStr, String prototype) { - // Input validation - Program program = getCurrentProgram(); - if (program == null) - return new PrototypeResult(false, "No program loaded"); - if (functionAddrStr == null || functionAddrStr.isEmpty()) { - return new PrototypeResult(false, "Function address is required"); - } - if (prototype == null || prototype.isEmpty()) { - return new PrototypeResult(false, "Function prototype is required"); - } - - final StringBuilder errorMessage = new StringBuilder(); - final AtomicBoolean success = new AtomicBoolean(false); - - try { - SwingUtilities.invokeAndWait( - () -> applyFunctionPrototype(program, functionAddrStr, prototype, success, errorMessage)); - } catch (InterruptedException | InvocationTargetException e) { - String msg = "Failed to set function prototype on Swing thread: " + e.getMessage(); - errorMessage.append(msg); - Msg.error(this, msg, e); - } - - return new PrototypeResult(success.get(), errorMessage.toString()); - } - - /** - * Helper method that applies the function prototype within a transaction - */ - private void applyFunctionPrototype(Program program, String functionAddrStr, String prototype, - AtomicBoolean success, StringBuilder errorMessage) { - try { - // Get the address and function - Address addr = program.getAddressFactory().getAddress(functionAddrStr); - Function func = getFunctionForAddress(program, addr); - - if (func == null) { - String msg = "Could not find function at address: " + functionAddrStr; - errorMessage.append(msg); - Msg.error(this, msg); - return; - } - - Msg.info(this, "Setting prototype for function " + func.getName() + ": " + prototype); - - // Store original prototype as a comment for reference - addPrototypeComment(program, func, prototype); - - // Use ApplyFunctionSignatureCmd to parse and apply the signature - parseFunctionSignatureAndApply(program, addr, prototype, success, errorMessage); - - } catch (Exception e) { - String msg = "Error setting function prototype: " + e.getMessage(); - errorMessage.append(msg); - Msg.error(this, msg, e); - } - } - - /** - * Add a comment showing the prototype being set - */ - private void addPrototypeComment(Program program, Function func, String prototype) { - int txComment = program.startTransaction("Add prototype comment"); - try { - program.getListing().setComment( - func.getEntryPoint(), - CodeUnit.PLATE_COMMENT, - "Setting prototype: " + prototype); - } finally { - program.endTransaction(txComment, true); - } - } - - /** - * Parse and apply the function signature with error handling - */ - private void parseFunctionSignatureAndApply(Program program, Address addr, String prototype, - AtomicBoolean success, StringBuilder errorMessage) { - // Use ApplyFunctionSignatureCmd to parse and apply the signature - int txProto = program.startTransaction("Set function prototype"); - try { - // Get data type manager - DataTypeManager dtm = program.getDataTypeManager(); - - // Get data type manager service - ghidra.app.services.DataTypeManagerService dtms = tool - .getService(ghidra.app.services.DataTypeManagerService.class); - - // Create function signature parser - ghidra.app.util.parser.FunctionSignatureParser parser = new ghidra.app.util.parser.FunctionSignatureParser( - dtm, dtms); - - // Parse the prototype into a function signature - ghidra.program.model.data.FunctionDefinitionDataType sig = parser.parse(null, prototype); - - if (sig == null) { - String msg = "Failed to parse function prototype"; - errorMessage.append(msg); - Msg.error(this, msg); - return; - } - - // Create and apply the command - ghidra.app.cmd.function.ApplyFunctionSignatureCmd cmd = new ghidra.app.cmd.function.ApplyFunctionSignatureCmd( - addr, sig, SourceType.USER_DEFINED); - - // Apply the command to the program - boolean cmdResult = cmd.applyTo(program, new ConsoleTaskMonitor()); - - if (cmdResult) { - success.set(true); - Msg.info(this, "Successfully applied function signature"); - } else { - String msg = "Command failed: " + cmd.getStatusMsg(); - errorMessage.append(msg); - Msg.error(this, msg); - } - } catch (Exception e) { - String msg = "Error applying function signature: " + e.getMessage(); - errorMessage.append(msg); - Msg.error(this, msg, e); - } finally { - program.endTransaction(txProto, success.get()); - } - } - - /** - * Set a local variable's type using HighFunctionDBUtil.updateDBVariable - */ - private boolean setLocalVariableType(String functionAddrStr, String variableName, String newType) { - // Input validation - Program program = getCurrentProgram(); - if (program == null) - return false; - if (functionAddrStr == null || functionAddrStr.isEmpty() || - variableName == null || variableName.isEmpty() || - newType == null || newType.isEmpty()) { - return false; - } - - AtomicBoolean success = new AtomicBoolean(false); - - try { - SwingUtilities - .invokeAndWait(() -> applyVariableType(program, functionAddrStr, variableName, newType, success)); - } catch (InterruptedException | InvocationTargetException e) { - Msg.error(this, "Failed to execute set variable type on Swing thread", e); - } - - return success.get(); - } - - /** - * Helper method that performs the actual variable type change - */ - private void applyVariableType(Program program, String functionAddrStr, - String variableName, String newType, AtomicBoolean success) { - try { - // Find the function - Address addr = program.getAddressFactory().getAddress(functionAddrStr); - Function func = getFunctionForAddress(program, addr); - - if (func == null) { - Msg.error(this, "Could not find function at address: " + functionAddrStr); - return; - } - - DecompileResults results = decompileFunction(func, program); - if (results == null || !results.decompileCompleted()) { - return; - } - - ghidra.program.model.pcode.HighFunction highFunction = results.getHighFunction(); - if (highFunction == null) { - Msg.error(this, "No high function available"); - return; - } - - // Find the symbol by name - HighSymbol symbol = findSymbolByName(highFunction, variableName); - if (symbol == null) { - Msg.error(this, "Could not find variable '" + variableName + "' in decompiled function"); - return; - } - - // Get high variable - HighVariable highVar = symbol.getHighVariable(); - if (highVar == null) { - Msg.error(this, "No HighVariable found for symbol: " + variableName); - return; - } - - Msg.info(this, "Found high variable for: " + variableName + - " with current type " + highVar.getDataType().getName()); - - // Find the data type - DataTypeManager dtm = program.getDataTypeManager(); - DataType dataType = resolveDataType(dtm, newType); - - if (dataType == null) { - Msg.error(this, "Could not resolve data type: " + newType); - return; - } - - Msg.info(this, "Using data type: " + dataType.getName() + " for variable " + variableName); - - // Apply the type change in a transaction - updateVariableType(program, symbol, dataType, success); - - } catch (Exception e) { - Msg.error(this, "Error setting variable type: " + e.getMessage()); - } - } - - /** - * Find a high symbol by name in the given high function - */ - private HighSymbol findSymbolByName(ghidra.program.model.pcode.HighFunction highFunction, String variableName) { - Iterator symbols = highFunction.getLocalSymbolMap().getSymbols(); - while (symbols.hasNext()) { - HighSymbol s = symbols.next(); - if (s.getName().equals(variableName)) { - return s; - } - } - return null; - } - - /** - * Decompile a function and return the results - */ - private DecompileResults decompileFunction(Function func, Program program) { - // Set up decompiler for accessing the decompiled function - DecompInterface decomp = new DecompInterface(); - decomp.openProgram(program); - decomp.setSimplificationStyle("decompile"); // Full decompilation - - // Decompile the function - DecompileResults results = decomp.decompileFunction(func, this.decompileTimeout, new ConsoleTaskMonitor()); - - if (!results.decompileCompleted()) { - Msg.error(this, "Could not decompile function: " + results.getErrorMessage()); - return null; - } - - return results; - } - - /** - * Apply the type update in a transaction - */ - private void updateVariableType(Program program, HighSymbol symbol, DataType dataType, AtomicBoolean success) { - int tx = program.startTransaction("Set variable type"); - try { - // Use HighFunctionDBUtil to update the variable with the new type - HighFunctionDBUtil.updateDBVariable( - symbol, // The high symbol to modify - symbol.getName(), // Keep original name - dataType, // The new data type - SourceType.USER_DEFINED // Mark as user-defined - ); - - success.set(true); - Msg.info(this, "Successfully set variable type using HighFunctionDBUtil"); - } catch (Exception e) { - Msg.error(this, "Error setting variable type: " + e.getMessage()); - } finally { - program.endTransaction(tx, success.get()); - } - } - - /** - * Get all references to a specific address (xref to) - */ - private String getXrefsTo(String addressStr, int offset, int limit) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - if (addressStr == null || addressStr.isEmpty()) - return "Address is required"; - - try { - Address addr = program.getAddressFactory().getAddress(addressStr); - ReferenceManager refManager = program.getReferenceManager(); - - ReferenceIterator refIter = refManager.getReferencesTo(addr); - - List refs = new ArrayList<>(); - while (refIter.hasNext()) { - Reference ref = refIter.next(); - Address fromAddr = ref.getFromAddress(); - RefType refType = ref.getReferenceType(); - - Function fromFunc = program.getFunctionManager().getFunctionContaining(fromAddr); - String funcInfo = (fromFunc != null) ? " in " + fromFunc.getName() : ""; - - refs.add(String.format("From %s%s [%s]", fromAddr, funcInfo, refType.getName())); - } - - return paginateList(refs, offset, limit); - } catch (Exception e) { - return "Error getting references to address: " + e.getMessage(); - } - } - - /** - * Get all references from a specific address (xref from) - */ - private String getXrefsFrom(String addressStr, int offset, int limit) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - if (addressStr == null || addressStr.isEmpty()) - return "Address is required"; - - try { - Address addr = program.getAddressFactory().getAddress(addressStr); - ReferenceManager refManager = program.getReferenceManager(); - - Reference[] references = refManager.getReferencesFrom(addr); - - List refs = new ArrayList<>(); - for (Reference ref : references) { - Address toAddr = ref.getToAddress(); - RefType refType = ref.getReferenceType(); - - String targetInfo = ""; - Function toFunc = program.getFunctionManager().getFunctionAt(toAddr); - if (toFunc != null) { - targetInfo = " to function " + toFunc.getName(); - } else { - Data data = program.getListing().getDataAt(toAddr); - if (data != null) { - targetInfo = " to data " + (data.getLabel() != null ? data.getLabel() : data.getPathName()); - } - } - - refs.add(String.format("To %s%s [%s]", toAddr, targetInfo, refType.getName())); - } - - return paginateList(refs, offset, limit); - } catch (Exception e) { - return "Error getting references from address: " + e.getMessage(); - } - } - - /** - * Get all references to a specific function by name - */ - private String getFunctionXrefs(String functionName, int offset, int limit) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - if (functionName == null || functionName.isEmpty()) - return "Function name is required"; - - try { - List refs = new ArrayList<>(); - FunctionManager funcManager = program.getFunctionManager(); - for (Function function : funcManager.getFunctions(true)) { - if (function.getName().equals(functionName)) { - Address entryPoint = function.getEntryPoint(); - ReferenceIterator refIter = program.getReferenceManager().getReferencesTo(entryPoint); - - while (refIter.hasNext()) { - Reference ref = refIter.next(); - Address fromAddr = ref.getFromAddress(); - RefType refType = ref.getReferenceType(); - - Function fromFunc = funcManager.getFunctionContaining(fromAddr); - String funcInfo = (fromFunc != null) ? " in " + fromFunc.getName() : ""; - - refs.add(String.format("From %s%s [%s]", fromAddr, funcInfo, refType.getName())); - } - } - } - - if (refs.isEmpty()) { - return "No references found to function: " + functionName; - } - - return paginateList(refs, offset, limit); - } catch (Exception e) { - return "Error getting function references: " + e.getMessage(); - } - } - - /** - * List all defined strings in the program with their addresses - */ - private String listDefinedStrings(int offset, int limit, String filter) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - - List lines = new ArrayList<>(); - DataIterator dataIt = program.getListing().getDefinedData(true); - - while (dataIt.hasNext()) { - Data data = dataIt.next(); - - if (data != null && isStringData(data)) { - String value = data.getValue() != null ? data.getValue().toString() : ""; - - if (filter == null || value.toLowerCase().contains(filter.toLowerCase())) { - String escapedValue = escapeString(value); - lines.add(String.format("%s: \"%s\"", data.getAddress(), escapedValue)); - } - } - } - - return paginateList(lines, offset, limit); - } - - /** - * Check if the given data is a string type - */ - private boolean isStringData(Data data) { - if (data == null) - return false; - - DataType dt = data.getDataType(); - String typeName = dt.getName().toLowerCase(); - return typeName.contains("string") || typeName.contains("char") || typeName.equals("unicode"); - } - - /** - * Escape special characters in a string for display - */ - private String escapeString(String input) { - if (input == null) - return ""; - - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < input.length(); i++) { - char c = input.charAt(i); - if (c >= 32 && c < 127) { - sb.append(c); - } else if (c == '\n') { - sb.append("\\n"); - } else if (c == '\r') { - sb.append("\\r"); - } else if (c == '\t') { - sb.append("\\t"); - } else { - sb.append(String.format("\\x%02x", (int) c & 0xFF)); - } - } - return sb.toString(); - } - - /** Retrieve address and data value by specifying a label name */ - private String getDataByLabel(String label) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - if (label == null || label.isEmpty()) - return "Label is required"; - - SymbolTable st = program.getSymbolTable(); - SymbolIterator it = st.getSymbols(label); - if (!it.hasNext()) - return "Label not found: " + label; - - StringBuilder sb = new StringBuilder(); - while (it.hasNext()) { - Symbol s = it.next(); - Address a = s.getAddress(); - Data d = program.getListing().getDefinedDataAt(a); - String v = (d != null) ? escapeString(String.valueOf(d.getDefaultValueRepresentation())) - : "(no defined data)"; - sb.append(String.format("%s -> %s : %s%n", label, a, v)); - } - return sb.toString(); - } - - /** Read memory by specifying address and size (results in Hexdump style) */ - private String getBytes(String addressStr, int size) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - if (addressStr == null || addressStr.isEmpty()) - return "Address is required"; - if (size <= 0) - return "Size must be > 0"; - - try { - Address addr = program.getAddressFactory().getAddress(addressStr); - byte[] buf = new byte[size]; - int read = program.getMemory().getBytes(addr, buf); - return hexdump(addr, buf, read); - } catch (Exception e) { - return "Error reading memory: " + e.getMessage(); - } - } - - /** Scan the entire program to search for a byte sequence */ - private String searchBytes(String bytesHex, int offset, int limit) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - if (bytesHex == null || bytesHex.isEmpty()) - return "Byte sequence required"; - - byte[] needle; - try { - needle = decodeHex(bytesHex); - } catch (IllegalArgumentException e) { - return "Invalid hex string: " + bytesHex; - } - - Memory mem = program.getMemory(); - List hits = new ArrayList<>(); - - Address cur = mem.getMinAddress(); - while (cur != null && hits.size() < offset + limit) { - Address found = mem.findBytes(cur, needle, null, true, TaskMonitorAdapter.DUMMY_MONITOR); - if (found == null) - break; - hits.add(found.toString()); - - cur = found.add(1); - } - - return paginateList(hits, offset, limit); - } - - private String hexdump(Address base, byte[] buf, int len) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < len; i += 16) { - sb.append(String.format("%s ", base.add(i))); - for (int j = 0; j < 16 && (i + j) < len; j++) { - sb.append(String.format("%02X ", buf[i + j])); - } - sb.append('\n'); - } - return sb.toString(); - } - - private byte[] decodeHex(String hex) { - hex = hex.replaceAll("\\s+", ""); - if (hex.length() % 2 != 0) - throw new IllegalArgumentException(); - byte[] out = new byte[hex.length() / 2]; - for (int i = 0; i < out.length; i++) { - out[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16); - } - return out; - } - - /** - * Resolves a data type by name, handling common types and pointer types - * - * @param dtm The data type manager - * @param typeName The type name to resolve - * @return The resolved DataType, or null if not found - */ - private DataType resolveDataType(DataTypeManager dtm, String typeName) { - DataTypeManagerService dtms = tool.getService(DataTypeManagerService.class); - DataTypeManager[] managers = dtms.getDataTypeManagers(); - DataType dt = null; - - List managerList = new ArrayList<>(); - for (DataTypeManager manager : managers) { - if (manager != dtm) - managerList.add(manager); - } - managerList.addFirst(dtm); - - DataTypeParser parser = null; - - for (DataTypeManager manager : managerList) { - try { - parser = new DataTypeParser(manager, null, null, AllowedDataTypes.ALL); - dt = parser.parse(typeName); - if (dt != null) { - return dt; // Found a successful parse, return - } - } catch (Exception e) { - // Continue to next manager if this one fails - } - } - - // Fallback to int if we couldn't find it - Msg.warn(this, "Unknown type: " + typeName + ", defaulting to int"); - return dtm.getDataType("/int"); - } - - /** - * Find a data type by name in all categories/folders of the data type manager - * This searches through all categories rather than just the root - */ - private DataType findDataTypeByNameInAllCategories(DataTypeManager dtm, String typeName) { - // Try exact match first - DataType result = searchByNameInAllCategories(dtm, typeName); - if (result != null) { - return result; - } - - // Try lowercase - return searchByNameInAllCategories(dtm, typeName.toLowerCase()); - } - - /** - * Helper method to search for a data type by name in all categories - */ - private DataType searchByNameInAllCategories(DataTypeManager dtm, String name) { - // Get all data types from the manager - Iterator allTypes = dtm.getAllDataTypes(); - while (allTypes.hasNext()) { - DataType dt = allTypes.next(); - // Check if the name matches exactly (case-sensitive) - if (dt.getName().equals(name)) { - return dt; - } - // For case-insensitive, we want an exact match except for case - if (dt.getName().equalsIgnoreCase(name)) { - return dt; - } - } - return null; - } - - private static class StructMember { - String name; - String type; - String comment; - double offset = -1; // Use double to handle GSON parsing number as double - } - - private String createStruct(String name, String category, int size, String membersJson) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - - final AtomicReference result = new AtomicReference<>(); - try { - SwingUtilities.invokeAndWait(() -> { - int txId = program.startTransaction("Create Struct"); - boolean success = false; - try { - DataTypeManager dtm = program.getDataTypeManager(); - CategoryPath path = new CategoryPath(category == null ? "/" : category); - - if (dtm.getDataType(path, name) != null) { - result.set("Error: Struct " + name + " already exists in category " + path); - return; - } - StructureDataType newStruct = new StructureDataType(path, name, size, dtm); - - StringBuilder responseBuilder = new StringBuilder( - "Struct " + name + " created successfully in category " + path); - - if (membersJson != null && !membersJson.isEmpty()) { - Gson gson = new Gson(); - StructMember[] members = gson.fromJson(membersJson, StructMember[].class); - - int membersAdded = 0; - for (StructMember member : members) { - DataType memberDt = resolveDataType(dtm, member.type); - if (memberDt == null) { - responseBuilder.append("\nError: Could not resolve data type '").append(member.type) - .append("' for member '").append(member.name) - .append("'. Aborting further member creation."); - break; - } - - if (member.offset != -1) { - newStruct.insertAtOffset((int) member.offset, memberDt, -1, member.name, - member.comment); - } else { - newStruct.add(memberDt, member.name, member.comment); - } - membersAdded++; - } - responseBuilder.append("\nAdded ").append(membersAdded).append(" members."); - } - dtm.addDataType(newStruct, DataTypeConflictHandler.DEFAULT_HANDLER); - result.set(responseBuilder.toString()); - success = true; - } catch (Exception e) { - result.set("Error: Failed to create struct: " + e.getMessage()); - } finally { - program.endTransaction(txId, success); - } - }); - } catch (InterruptedException | InvocationTargetException e) { - return "Error: Failed to execute create struct on Swing thread: " + e.getMessage(); - } - return result.get(); - } - - private String addStructMembers(String structName, String category, String membersJson) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - - final AtomicReference result = new AtomicReference<>(); - try { - SwingUtilities.invokeAndWait(() -> { - int txId = program.startTransaction("Add Struct Member"); - boolean success = false; - try { - DataTypeManager dtm = program.getDataTypeManager(); - CategoryPath path = new CategoryPath(category == null ? "/" : category); - DataType dt = dtm.getDataType(path, structName); - - if (dt == null || !(dt instanceof Structure)) { - result.set("Error: Struct " + structName + " not found in category " + path); - return; - } - Structure struct = (Structure) dt; - - StringBuilder responseBuilder = new StringBuilder(); - - if (membersJson != null && !membersJson.isEmpty()) { - Gson gson = new Gson(); - StructMember[] members = gson.fromJson(membersJson, StructMember[].class); - - int membersAdded = 0; - for (StructMember member : members) { - DataType memberDt = resolveDataType(dtm, member.type); - if (memberDt == null) { - responseBuilder.append("\nError: Could not resolve data type '").append(member.type) - .append("' for member '").append(member.name) - .append("'. Aborting further member creation."); - break; - } - - if (member.offset != -1) { - struct.insertAtOffset((int) member.offset, memberDt, -1, member.name, member.comment); - } else { - struct.add(memberDt, member.name, member.comment); - } - membersAdded++; - } - responseBuilder.append("\nAdded ").append(membersAdded).append(" members."); - result.set(responseBuilder.toString()); - success = membersAdded > 0; - } - - } catch (Exception e) { - result.set("Error: Failed to add member to struct: " + e.getMessage()); - } finally { - program.endTransaction(txId, success); - } - }); - } catch (InterruptedException | InvocationTargetException e) { - return "Error: Failed to execute add struct member on Swing thread: " + e.getMessage(); - } - return result.get(); - } - - private String clearStruct(String structName, String category) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - - final AtomicReference result = new AtomicReference<>(); - try { - SwingUtilities.invokeAndWait(() -> { - int txId = program.startTransaction("Clear Struct"); - boolean success = false; - try { - DataTypeManager dtm = program.getDataTypeManager(); - CategoryPath path = new CategoryPath(category == null ? "/" : category); - DataType dt = dtm.getDataType(path, structName); - if (dt == null || !(dt instanceof Structure)) { - result.set("Error: Struct " + structName + " not found in category " + path); - return; - } - Structure struct = (Structure) dt; - if (struct.isNotYetDefined()) { - result.set("Struct " + structName + " is empty, nothing to clear."); - success = true; // Not an error state - return; - } - struct.deleteAll(); - result.set("Struct " + structName + " cleared."); - success = true; - } catch (Exception e) { - result.set("Error: Failed to clear struct: " + e.getMessage()); - } finally { - program.endTransaction(txId, success); - } - }); - } catch (InterruptedException | InvocationTargetException e) { - return "Error: Failed to execute clear struct on Swing thread: " + e.getMessage(); - } - return result.get(); - } - - private String getStruct(String structName, String category) { - Program program = getCurrentProgram(); - if (program == null) - return "No program loaded"; - - DataTypeManager dtm = program.getDataTypeManager(); - CategoryPath path = new CategoryPath(category == null ? "/" : category); - DataType dt = dtm.getDataType(path, structName); - - if (dt == null || !(dt instanceof Structure)) { - return "Error: Struct " + structName + " not found in category " + path; - } - - Structure struct = (Structure) dt; - - Map structRepr = new HashMap<>(); - structRepr.put("name", struct.getName()); - structRepr.put("category", struct.getCategoryPath().getPath()); - structRepr.put("size", struct.getLength()); - structRepr.put("isNotYetDefined", struct.isNotYetDefined()); - - List> membersList = new ArrayList<>(); - for (DataTypeComponent component : struct.getDefinedComponents()) { - Map memberMap = new HashMap<>(); - memberMap.put("name", component.getFieldName()); - memberMap.put("type", component.getDataType().getName()); - memberMap.put("offset", component.getOffset()); - memberMap.put("size", component.getLength()); - memberMap.put("comment", component.getComment()); - membersList.add(memberMap); - } - structRepr.put("members", membersList); - - Gson gson = new Gson(); - return gson.toJson(structRepr); - } - - // ---------------------------------------------------------------------------------- - // Utility: parse query params, parse post params, pagination, etc. - // ---------------------------------------------------------------------------------- - - /** - * Parses HTTP query parameters from the request URL into a key-value map. - * - * This method extracts query parameters from the URL (the portion after the '?' - * character) - * and decodes them using UTF-8 encoding. It handles URL-encoded parameter - * values and - * properly decodes special characters. - * - * Supported Format: {@code ?offset=10&limit=100&filter=test%20string} - * - * Example Usage: - * - *
{@code
-	 * // For URL: /methods?offset=50&limit=25
-	 * Map params = parseQueryParams(exchange);
-	 * int offset = parseIntOrDefault(params.get("offset"), 0); // Returns 50
-	 * int limit = parseIntOrDefault(params.get("limit"), 100); // Returns 25
-	 * }
- * - * @param exchange The HTTP exchange containing the request with query - * parameters - * @return A map of parameter names to their decoded values. Empty map if no - * parameters exist. - * Parameters without values are ignored. - * @see URLDecoder#decode(String, Charset) - * @see #parseIntOrDefault(String, int) - */ - private Map parseQueryParams(HttpExchange exchange) { - Map result = new HashMap<>(); - String query = exchange.getRequestURI().getQuery(); // e.g. offset=10&limit=100 - if (query != null) { - String[] pairs = query.split("&"); - for (String p : pairs) { - String[] kv = p.split("="); - if (kv.length == 2) { - // URL decode parameter values - try { - String key = URLDecoder.decode(kv[0], StandardCharsets.UTF_8); - String value = URLDecoder.decode(kv[1], StandardCharsets.UTF_8); - result.put(key, value); - } catch (Exception e) { - Msg.error(this, "Error decoding URL parameter", e); - } - } - } - } - return result; - } - - /** - * Parse post body form params, e.g. oldName=foo&newName=bar - */ - private Map parsePostParams(HttpExchange exchange) throws IOException { - byte[] body = exchange.getRequestBody().readAllBytes(); - String bodyStr = new String(body, StandardCharsets.UTF_8); - Map params = new HashMap<>(); - for (String pair : bodyStr.split("&")) { - String[] kv = pair.split("="); - if (kv.length == 2) { - // URL decode parameter values - try { - String key = URLDecoder.decode(kv[0], StandardCharsets.UTF_8); - String value = URLDecoder.decode(kv[1], StandardCharsets.UTF_8); - params.put(key, value); - } catch (Exception e) { - Msg.error(this, "Error decoding URL parameter", e); - } - } - } - return params; - } - - /** - * Applies pagination to a list of strings and returns a formatted result. - * - * This utility method implements standard pagination behavior by: - *
    - *
  1. Validating and constraining the offset to valid bounds
  2. - *
  3. Calculating the end index based on offset + limit
  4. - *
  5. Extracting the requested subset of items
  6. - *
  7. Joining the results with newline characters
  8. - *
- * - * Pagination Logic: - *
    - *
  • If offset >= list size, returns empty string
  • - *
  • If offset + limit exceeds list size, returns items from offset to - * end
  • - *
  • Negative offsets are treated as 0
  • - *
- * - * Example: - * - *
{@code
-	 * List functions = Arrays.asList("main", "printf", "malloc", "free", "exit");
-	 * String page1 = paginateList(functions, 0, 2); // Returns "main\nprintf"
-	 * String page2 = paginateList(functions, 2, 2); // Returns "malloc\nfree"
-	 * String page3 = paginateList(functions, 4, 2); // Returns "exit"
-	 * }
- * - * @param items The complete list of string items to paginate - * @param offset Starting index for pagination (0-based, negative values treated - * as 0) - * @param limit Maximum number of items to return - * @return Newline-separated string of the requested items, or empty string if - * no items in range - * @see List#subList(int, int) - */ - private String paginateList(List items, int offset, int limit) { - int start = Math.max(0, offset); - int end = Math.min(items.size(), offset + limit); - - if (start >= items.size()) { - return ""; // no items in range - } - List sub = items.subList(start, end); - return String.join("\n", sub); - } - - /** - * Parse an integer from a string, or return defaultValue if null/invalid. - */ - private int parseIntOrDefault(String val, int defaultValue) { - if (val == null) - return defaultValue; - try { - return Integer.parseInt(val); - } catch (NumberFormatException e) { - return defaultValue; - } - } - - /** - * Escape non-ASCII chars to avoid potential decode issues. - */ - private String escapeNonAscii(String input) { - if (input == null) - return ""; - StringBuilder sb = new StringBuilder(); - for (char c : input.toCharArray()) { - if (c >= 32 && c < 127) { - sb.append(c); - } else { - sb.append("\\x"); - sb.append(Integer.toHexString(c & 0xFF)); - } - } - return sb.toString(); - } - - /** - * Retrieves the currently active program from Ghidra's program manager. - * - * This method provides safe access to the current program being analyzed in - * the CodeBrowser. It handles cases where no program is loaded or the - * ProgramManager service is unavailable. - * - * Usage Pattern: This method is called by virtually every API endpoint - * to ensure operations are performed on the correct program context. - * - * @return The currently loaded Program instance, or {@code null} if: - *
    - *
  • No program is currently loaded in CodeBrowser
  • - *
  • ProgramManager service is not available
  • - *
  • Plugin is not properly initialized
  • - *
- * @see ProgramManager#getCurrentProgram() - * @see PluginTool#getService(Class) - */ - public Program getCurrentProgram() { - ProgramManager pm = tool.getService(ProgramManager.class); - return pm != null ? pm.getCurrentProgram() : null; - } - - private void sendResponse(HttpExchange exchange, String response) throws IOException { - byte[] bytes = response.getBytes(StandardCharsets.UTF_8); - exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8"); - exchange.sendResponseHeaders(200, bytes.length); - try (OutputStream os = exchange.getResponseBody()) { - os.write(bytes); - } - } - /** * Cleanly shuts down the HTTP server and releases plugin resources. * diff --git a/src/main/java/com/lauriewired/handlers/Handler.java b/src/main/java/com/lauriewired/handlers/Handler.java new file mode 100644 index 0000000..1ed4b64 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/Handler.java @@ -0,0 +1,48 @@ +package com.lauriewired.handlers; + +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; + +/** + * Abstract class representing a handler for HTTP requests in a Ghidra + * PluginTool. + * Subclasses must implement the handle method to define how requests are + * processed. + */ +public abstract class Handler { + /** The PluginTool instance this handler is associated with. */ + protected final PluginTool tool; + + /** The path this handler will respond to. */ + protected final String path; + + /** + * Constructs a new Handler with the specified PluginTool and path. + * + * @param tool the PluginTool instance this handler is associated with + * @param path the path this handler will respond to + */ + protected Handler(PluginTool tool, String path) { + this.tool = tool; + this.path = path; + } + + /** + * Gets the path this handler will respond to. + * + * @return the path + */ + public String getPath() { + return path; + } + + /** + * Handles an HTTP request. + * Subclasses must implement this method to define how requests are + * processed. + * + * @param exchange the HttpExchange object representing the HTTP request + * @throws Exception if an error occurs while handling the request + */ + public abstract void handle(HttpExchange exchange) throws Exception; +} diff --git a/src/main/java/com/lauriewired/handlers/act/AddStructMembers.java b/src/main/java/com/lauriewired/handlers/act/AddStructMembers.java new file mode 100644 index 0000000..5f1e97b --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/act/AddStructMembers.java @@ -0,0 +1,104 @@ +package com.lauriewired.handlers.act; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.data.DataType; +import ghidra.program.model.data.DataTypeManager; +import ghidra.program.model.data.Structure; +import ghidra.program.model.listing.Program; + +import com.google.gson.Gson; + +import javax.swing.SwingUtilities; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; + +import static com.lauriewired.util.GhidraUtils.*; +import static com.lauriewired.util.ParseUtils.*; +import static com.lauriewired.util.StructUtils.StructMember; +import ghidra.program.model.data.CategoryPath; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +public final class AddStructMembers extends Handler { + public AddStructMembers(PluginTool tool) { + super(tool, "/add_struct_members"); + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + Map params = parsePostParams(exchange); + String structName = params.get("struct_name"); + String category = params.get("category"); + String membersJson = params.get("members"); + + if (structName == null || membersJson == null) { + sendResponse(exchange, "struct_name and members are required"); + return; + } + sendResponse(exchange, addStructMembers(structName, category, membersJson)); + } + + private String addStructMembers(String structName, String category, String membersJson) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + + final AtomicReference result = new AtomicReference<>(); + try { + SwingUtilities.invokeAndWait(() -> { + int txId = program.startTransaction("Add Struct Member"); + boolean success = false; + try { + DataTypeManager dtm = program.getDataTypeManager(); + CategoryPath path = new CategoryPath(category == null ? "/" : category); + DataType dt = dtm.getDataType(path, structName); + + if (dt == null || !(dt instanceof Structure)) { + result.set("Error: Struct " + structName + " not found in category " + path); + return; + } + Structure struct = (Structure) dt; + + StringBuilder responseBuilder = new StringBuilder(); + + if (membersJson != null && !membersJson.isEmpty()) { + Gson gson = new Gson(); + StructMember[] members = gson.fromJson(membersJson, StructMember[].class); + + int membersAdded = 0; + for (StructMember member : members) { + DataType memberDt = resolveDataType(tool, dtm, member.type); + if (memberDt == null) { + responseBuilder.append("\nError: Could not resolve data type '").append(member.type) + .append("' for member '").append(member.name) + .append("'. Aborting further member creation."); + break; + } + + if (member.offset != -1) { + struct.insertAtOffset((int) member.offset, memberDt, -1, member.name, member.comment); + } else { + struct.add(memberDt, member.name, member.comment); + } + membersAdded++; + } + responseBuilder.append("\nAdded ").append(membersAdded).append(" members."); + result.set(responseBuilder.toString()); + success = membersAdded > 0; + } + + } catch (Exception e) { + result.set("Error: Failed to add member to struct: " + e.getMessage()); + } finally { + program.endTransaction(txId, success); + } + }); + } catch (InterruptedException | InvocationTargetException e) { + return "Error: Failed to execute add struct member on Swing thread: " + e.getMessage(); + } + return result.get(); + } +} diff --git a/src/main/java/com/lauriewired/handlers/act/CreateStruct.java b/src/main/java/com/lauriewired/handlers/act/CreateStruct.java new file mode 100644 index 0000000..a54e784 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/act/CreateStruct.java @@ -0,0 +1,106 @@ +package com.lauriewired.handlers.act; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.data.DataType; +import ghidra.program.model.data.DataTypeConflictHandler; +import ghidra.program.model.data.DataTypeManager; +import ghidra.program.model.data.StructureDataType; +import ghidra.program.model.listing.Program; + +import com.google.gson.Gson; + +import javax.swing.SwingUtilities; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; + +import static com.lauriewired.util.GhidraUtils.*; +import static com.lauriewired.util.ParseUtils.*; +import static com.lauriewired.util.StructUtils.StructMember; +import ghidra.program.model.data.CategoryPath; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +public final class CreateStruct extends Handler { + public CreateStruct(PluginTool tool) { + super(tool, "/create_struct"); + } + + public void handle(HttpExchange exchange) throws IOException { + Map params = parsePostParams(exchange); + String name = params.get("name"); + String category = params.get("category"); + long size = parseIntOrDefault(params.get("size"), 0); + String membersJson = params.get("members"); // Optional + + if (name == null || name.isEmpty()) { + sendResponse(exchange, "Struct name is required"); + return; + } + sendResponse(exchange, createStruct(name, category, (int) size, membersJson)); + } + + private String createStruct(String name, String category, int size, String membersJson) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + + final AtomicReference result = new AtomicReference<>(); + try { + SwingUtilities.invokeAndWait(() -> { + int txId = program.startTransaction("Create Struct"); + boolean success = false; + try { + DataTypeManager dtm = program.getDataTypeManager(); + CategoryPath path = new CategoryPath(category == null ? "/" : category); + + if (dtm.getDataType(path, name) != null) { + result.set("Error: Struct " + name + " already exists in category " + path); + return; + } + StructureDataType newStruct = new StructureDataType(path, name, size, dtm); + + StringBuilder responseBuilder = new StringBuilder( + "Struct " + name + " created successfully in category " + path); + + if (membersJson != null && !membersJson.isEmpty()) { + Gson gson = new Gson(); + StructMember[] members = gson.fromJson(membersJson, StructMember[].class); + + int membersAdded = 0; + for (StructMember member : members) { + DataType memberDt = resolveDataType(tool, dtm, member.type); + if (memberDt == null) { + responseBuilder.append("\nError: Could not resolve data type '").append(member.type) + .append("' for member '").append(member.name) + .append("'. Aborting further member creation."); + break; + } + + if (member.offset != -1) { + newStruct.insertAtOffset((int) member.offset, memberDt, -1, member.name, + member.comment); + } else { + newStruct.add(memberDt, member.name, member.comment); + } + membersAdded++; + } + responseBuilder.append("\nAdded ").append(membersAdded).append(" members."); + } + dtm.addDataType(newStruct, DataTypeConflictHandler.DEFAULT_HANDLER); + result.set(responseBuilder.toString()); + success = true; + } catch (Exception e) { + result.set("Error: Failed to create struct: " + e.getMessage()); + } finally { + program.endTransaction(txId, success); + } + }); + } catch (InterruptedException | InvocationTargetException e) { + return "Error: Failed to execute create struct on Swing thread: " + e.getMessage(); + } + return result.get(); + } +} diff --git a/src/main/java/com/lauriewired/handlers/act/DecompileFunctionByAddress.java b/src/main/java/com/lauriewired/handlers/act/DecompileFunctionByAddress.java new file mode 100644 index 0000000..68d2b34 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/act/DecompileFunctionByAddress.java @@ -0,0 +1,79 @@ +package com.lauriewired.handlers.act; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.app.decompiler.DecompInterface; +import ghidra.app.decompiler.DecompileResults; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Program; +import ghidra.util.task.ConsoleTaskMonitor; + +import java.io.IOException; +import java.util.Map; + +import static com.lauriewired.util.ParseUtils.parseQueryParams; +import static com.lauriewired.util.ParseUtils.sendResponse; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler to decompile a function by its address in the current program. + * This handler responds to HTTP requests with the decompiled C code of the function + * at the specified address. + */ +public final class DecompileFunctionByAddress extends Handler { + /** + * Constructor for the DecompileFunctionByAddress handler + * + * @param tool the PluginTool instance to interact with Ghidra + */ + public DecompileFunctionByAddress(PluginTool tool) { + super(tool, "/decompile_function"); + } + + /** + * Handles HTTP requests to decompile a function by its address. + * Expects a query parameter "address" with the function's address. + * + * @param exchange the HttpExchange object representing the HTTP request + * @throws IOException if an I/O error occurs during handling + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + Map qparams = parseQueryParams(exchange); + String address = qparams.get("address"); + sendResponse(exchange, decompileFunctionByAddress(address)); + } + + /** + * Decompiles the function at the specified address in the current program. + * + * @param addressStr the address of the function to decompile + * @return the decompiled C code or an error message + */ + private String decompileFunctionByAddress(String addressStr) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + if (addressStr == null || addressStr.isEmpty()) + return "Address is required"; + + try { + Address addr = program.getAddressFactory().getAddress(addressStr); + Function func = program.getListing().getFunctionContaining(addr); + if (func == null) + return "No function found at or containing address " + addressStr; + + DecompInterface decomp = new DecompInterface(); + decomp.openProgram(program); + DecompileResults result = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor()); + + return (result != null && result.decompileCompleted()) + ? result.getDecompiledFunction().getC() + : "Decompilation failed"; + } catch (Exception e) { + return "Error decompiling function: " + e.getMessage(); + } + } +} diff --git a/src/main/java/com/lauriewired/handlers/act/DecompileFunctionByName.java b/src/main/java/com/lauriewired/handlers/act/DecompileFunctionByName.java new file mode 100644 index 0000000..9dd7d30 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/act/DecompileFunctionByName.java @@ -0,0 +1,76 @@ +package com.lauriewired.handlers.act; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.app.decompiler.DecompInterface; +import ghidra.app.decompiler.DecompileOptions; +import ghidra.app.decompiler.DecompileResults; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Program; +import ghidra.util.task.ConsoleTaskMonitor; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static com.lauriewired.util.ParseUtils.sendResponse; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler to decompile a function by its name. + * Expects the function name in the request body. + */ +public final class DecompileFunctionByName extends Handler { + /** + * Constructs a new DecompileFunctionByName handler. + * + * @param tool The Ghidra plugin tool instance. + */ + public DecompileFunctionByName(PluginTool tool) { + super(tool, "/decompile"); + } + + /** + * Handles the HTTP request to decompile a function by its name. + * Reads the function name from the request body and returns the decompiled C + * pseudocode. + * + * @param exchange The HTTP exchange containing the request and response. + * @throws IOException If an I/O error occurs during handling. + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + String name = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + sendResponse(exchange, generateResponse(name)); + } + + /** + * Generates the decompiled C pseudocode for the function with the specified + * name. + * + * @param name The name of the function to decompile. + * @return The decompiled C pseudocode or an error message if the function is + * not found. + */ + private String generateResponse(String name) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + DecompInterface decomp = new DecompInterface(); + DecompileOptions options = new DecompileOptions(); + options.setRespectReadOnly(true); + decomp.setOptions(options); + decomp.openProgram(program); + for (Function func : program.getFunctionManager().getFunctions(true)) { + if (func.getName().equals(name)) { + DecompileResults result = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor()); + if (result != null && result.decompileCompleted()) { + return result.getDecompiledFunction().getC(); + } else { + return "Decompilation failed"; + } + } + } + return "Function not found"; + } +} diff --git a/src/main/java/com/lauriewired/handlers/act/DisassembleFunction.java b/src/main/java/com/lauriewired/handlers/act/DisassembleFunction.java new file mode 100644 index 0000000..c60c8f1 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/act/DisassembleFunction.java @@ -0,0 +1,90 @@ +package com.lauriewired.handlers.act; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.*; + +import java.io.IOException; +import java.util.Map; + +import static com.lauriewired.util.ParseUtils.parseQueryParams; +import static com.lauriewired.util.ParseUtils.sendResponse; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler for disassembling a function at a given address in Ghidra + * + * This handler responds to HTTP requests to disassemble a function + * and returns the assembly code as a string. + */ +public final class DisassembleFunction extends Handler { + /** + * Constructor for the DisassembleFunction handler + * + * @param tool the Ghidra plugin tool instance + */ + public DisassembleFunction(PluginTool tool) { + super(tool, "/disassemble_function"); + } + + /** + * Handles HTTP requests to disassemble a function at a specified address + * + * @param exchange the HTTP exchange containing the request + * @throws IOException if an I/O error occurs during handling + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + Map qparams = parseQueryParams(exchange); + String address = qparams.get("address"); + sendResponse(exchange, disassembleFunction(address)); + } + + /** + * Disassembles the function at the specified address and returns the assembly + * code + * + * @param addressStr the address of the function to disassemble + * @return a string containing the disassembled function code + */ + private String disassembleFunction(String addressStr) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + if (addressStr == null || addressStr.isEmpty()) + return "Address is required"; + + try { + Address addr = program.getAddressFactory().getAddress(addressStr); + Function func = program.getListing().getFunctionContaining(addr); + if (func == null) + return "No function found at or containing address " + addressStr; + + StringBuilder result = new StringBuilder(); + Listing listing = program.getListing(); + Address start = func.getEntryPoint(); + Address end = func.getBody().getMaxAddress(); + + InstructionIterator instructions = listing.getInstructions(start, true); + while (instructions.hasNext()) { + Instruction instr = instructions.next(); + if (instr.getAddress().compareTo(end) > 0) { + break; // Stop if we've gone past the end of the function + } + String comment = listing.getComment(CodeUnit.EOL_COMMENT, instr.getAddress()); + comment = (comment != null) ? "; " + comment : ""; + + result.append(String.format("%s: %s %s\n", + instr.getAddress(), + instr.toString(), + comment)); + } + + return result.toString(); + } catch (Exception e) { + return "Error disassembling function: " + e.getMessage(); + } + } +} diff --git a/src/main/java/com/lauriewired/handlers/comment/SetDecompilerComment.java b/src/main/java/com/lauriewired/handlers/comment/SetDecompilerComment.java new file mode 100644 index 0000000..0832d34 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/comment/SetDecompilerComment.java @@ -0,0 +1,53 @@ +package com.lauriewired.handlers.comment; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.CodeUnit; + +import java.util.Map; + +import static com.lauriewired.util.GhidraUtils.setCommentAtAddress; +import static com.lauriewired.util.ParseUtils.parsePostParams; +import static com.lauriewired.util.ParseUtils.sendResponse; + +/** + * Handler for setting a decompiler comment in Ghidra + * This handler processes HTTP requests to set comments on decompiled code + */ +public final class SetDecompilerComment extends Handler { + /** + * Constructor for the SetDecompilerComment handler + * + * @param tool The Ghidra PluginTool instance + */ + public SetDecompilerComment(PluginTool tool) { + super(tool, "/set_decompiler_comment"); + } + + /** + * Handles HTTP POST requests to set a decompiler comment + * + * @param exchange The HTTP exchange containing the request and response + * @throws Exception If an error occurs while processing the request + */ + @Override + public void handle(HttpExchange exchange) throws Exception { + Map params = parsePostParams(exchange); + String address = params.get("address"); + String comment = params.get("comment"); + boolean success = setDecompilerComment(address, comment); + sendResponse(exchange, success ? "Comment set successfully" : "Failed to set comment"); + } + + /** + * Sets a decompiler comment at the specified address + * + * @param addressStr The address as a string where the comment should be set + * @param comment The comment to set + * @return true if the comment was set successfully, false otherwise + */ + private boolean setDecompilerComment(String addressStr, String comment) { + return setCommentAtAddress(tool, addressStr, comment, CodeUnit.PRE_COMMENT, "Set decompiler comment"); + } +} diff --git a/src/main/java/com/lauriewired/handlers/comment/SetDisassemblyComment.java b/src/main/java/com/lauriewired/handlers/comment/SetDisassemblyComment.java new file mode 100644 index 0000000..f6c59f4 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/comment/SetDisassemblyComment.java @@ -0,0 +1,54 @@ +package com.lauriewired.handlers.comment; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.CodeUnit; + +import java.util.Map; + +import static com.lauriewired.util.GhidraUtils.setCommentAtAddress; +import static com.lauriewired.util.ParseUtils.parsePostParams; +import static com.lauriewired.util.ParseUtils.sendResponse; + +/** + * Handler for setting a comment in the disassembly at a specific address. + * Expects POST request with parameters: address and comment. + */ +public final class SetDisassemblyComment extends Handler { + /** + * Constructor for the SetDisassemblyComment handler. + * + * @param tool the Ghidra PluginTool instance + */ + public SetDisassemblyComment(PluginTool tool) { + super(tool, "/set_disassembly_comment"); + } + + /** + * Handles the HTTP request to set a disassembly comment. + * Expects a POST request with parameters: address and comment. + * + * @param exchange the HTTP exchange containing the request + * @throws Exception if an error occurs while handling the request + */ + @Override + public void handle(HttpExchange exchange) throws Exception { + Map params = parsePostParams(exchange); + String address = params.get("address"); + String comment = params.get("comment"); + boolean success = setDisassemblyComment(address, comment); + sendResponse(exchange, success ? "Comment set successfully" : "Failed to set comment"); + } + + /** + * Sets a disassembly comment at the specified address. + * + * @param addressStr the address as a string + * @param comment the comment to set + * @return true if the comment was set successfully, false otherwise + */ + private boolean setDisassemblyComment(String addressStr, String comment) { + return setCommentAtAddress(tool, addressStr, comment, CodeUnit.EOL_COMMENT, "Set disassembly comment"); + } +} diff --git a/src/main/java/com/lauriewired/handlers/get/GetAllClassNames.java b/src/main/java/com/lauriewired/handlers/get/GetAllClassNames.java new file mode 100644 index 0000000..9367163 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/get/GetAllClassNames.java @@ -0,0 +1,71 @@ +package com.lauriewired.handlers.get; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.Namespace; +import ghidra.program.model.symbol.Symbol; + +import java.io.IOException; +import java.util.*; + +import static com.lauriewired.util.ParseUtils.*; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler to get all class names in the current program. + * Supports pagination via 'offset' and 'limit' query parameters. + */ +public final class GetAllClassNames extends Handler { + /** + * Constructor for the GetAllClassNames handler. + * + * @param tool The PluginTool instance to use for accessing the current program. + */ + public GetAllClassNames(PluginTool tool) { + super(tool, "/classes"); + } + + /** + * Parses the query parameters from the HTTP request and returns a response + * containing + * all class names in the current program, with optional pagination. + * + * @param exchange The HttpExchange object representing the HTTP request. + * @throws IOException If an I/O error occurs while handling the request. + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + Map qparams = parseQueryParams(exchange); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + sendResponse(exchange, generateResponse(offset, limit)); + } + + /** + * Generates a response containing all class names in the current program, + * with optional pagination. + * + * @param offset The starting index for pagination. + * @param limit The maximum number of class names to return. + * @return A string containing the paginated list of class names. + */ + private String generateResponse(int offset, int limit) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + + Set classNames = new HashSet<>(); + for (Symbol symbol : program.getSymbolTable().getAllSymbols(true)) { + Namespace ns = symbol.getParentNamespace(); + if (ns != null && !ns.isGlobal()) { + classNames.add(ns.getName()); + } + } + // Convert set to list for pagination + List sorted = new ArrayList<>(classNames); + Collections.sort(sorted); + return paginateList(sorted, offset, limit); + } +} diff --git a/src/main/java/com/lauriewired/handlers/get/GetAllFunctionNames.java b/src/main/java/com/lauriewired/handlers/get/GetAllFunctionNames.java new file mode 100644 index 0000000..3a6ba82 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/get/GetAllFunctionNames.java @@ -0,0 +1,66 @@ +package com.lauriewired.handlers.get; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Program; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.lauriewired.util.ParseUtils.*; +import static com.lauriewired.util.ParseUtils.parseIntOrDefault; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler to get all function names in the current program. + * + * Example usage: GET /methods?offset=0&limit=100 + */ +public final class GetAllFunctionNames extends Handler { + /** + * Constructor for the GetAllFunctionNames handler. + * + * @param tool the PluginTool instance + */ + public GetAllFunctionNames(PluginTool tool) { + super(tool, "/methods"); + } + + /** + * Handles the HTTP request to get all function names. + * + * @param exchange the HttpExchange instance containing the request + * @throws IOException if an I/O error occurs + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + Map qparams = parseQueryParams(exchange); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + sendResponse(exchange, generateResponse(offset, limit)); + } + + /** + * Generates a paginated response containing all function names in the current + * program. + * + * @param offset the starting index for pagination + * @param limit the maximum number of function names to return + * @return a string containing the paginated list of function names + */ + private String generateResponse(int offset, int limit) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + + List names = new ArrayList<>(); + for (Function f : program.getFunctionManager().getFunctions(true)) { + names.add(f.getName()); + } + return paginateList(names, offset, limit); + } +} diff --git a/src/main/java/com/lauriewired/handlers/get/GetBytes.java b/src/main/java/com/lauriewired/handlers/get/GetBytes.java new file mode 100644 index 0000000..dc76557 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/get/GetBytes.java @@ -0,0 +1,68 @@ +package com.lauriewired.handlers.get; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; + +import java.io.IOException; +import java.util.*; + +import static com.lauriewired.util.ParseUtils.*; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler to get bytes from a specified address in the current program. + * Expects query parameters: address=
and size=. + */ +public final class GetBytes extends Handler { + /** + * Constructor for the GetBytes handler. + * + * @param tool The PluginTool instance to use. + */ + public GetBytes(PluginTool tool) { + super(tool, "/get_bytes"); + } + + /** + * Parses the query parameters from the HTTP exchange. + * + * @param exchange The HTTP exchange containing the request. + * @return A map of query parameters. + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + Map qparams = parseQueryParams(exchange); + String addrStr = qparams.get("address"); + int size = parseIntOrDefault(qparams.get("size"), 1); + sendResponse(exchange, getBytes(addrStr, size)); + } + + /** + * Gets the bytes from the specified address in the current program. + * + * @param addressStr The address to read from. + * @param size The number of bytes to read. + * @return A string representation of the bytes in hex format. + */ + private String getBytes(String addressStr, int size) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + if (addressStr == null || addressStr.isEmpty()) + return "Address is required"; + if (size <= 0) + return "Size must be > 0"; + + try { + Address addr = program.getAddressFactory().getAddress(addressStr); + byte[] buf = new byte[size]; + int read = program.getMemory().getBytes(addr, buf); + return hexdump(addr, buf, read); + } catch (Exception e) { + return "Error reading memory: " + e.getMessage(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/lauriewired/handlers/get/GetCurrentAddress.java b/src/main/java/com/lauriewired/handlers/get/GetCurrentAddress.java new file mode 100644 index 0000000..9370a52 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/get/GetCurrentAddress.java @@ -0,0 +1,50 @@ +package com.lauriewired.handlers.get; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.app.services.CodeViewerService; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.util.ProgramLocation; + +import java.io.IOException; + +import static com.lauriewired.util.ParseUtils.sendResponse; + +/** + * Handler to get the current address from the CodeViewerService + */ +public final class GetCurrentAddress extends Handler { + /** + * Constructor for GetCurrentAddress handler + * + * @param tool PluginTool instance to access Ghidra services + */ + public GetCurrentAddress(PluginTool tool) { + super(tool, "/get_current_address"); + } + + /** + * Handle HTTP request to get current address + * + * @param exchange HttpExchange instance containing request and response + * @throws IOException if an I/O error occurs + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + sendResponse(exchange, getCurrentAddress()); + } + + /** + * Retrieves the current address from the CodeViewerService + * + * @return String representation of the current address or an error message + */ + private String getCurrentAddress() { + CodeViewerService service = tool.getService(CodeViewerService.class); + if (service == null) + return "Code viewer service not available"; + + ProgramLocation location = service.getCurrentLocation(); + return (location != null) ? location.getAddress().toString() : "No current location"; + } +} diff --git a/src/main/java/com/lauriewired/handlers/get/GetCurrentFunction.java b/src/main/java/com/lauriewired/handlers/get/GetCurrentFunction.java new file mode 100644 index 0000000..2fa62fc --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/get/GetCurrentFunction.java @@ -0,0 +1,69 @@ +package com.lauriewired.handlers.get; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.app.services.CodeViewerService; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; + +import java.io.IOException; + +import static com.lauriewired.util.ParseUtils.sendResponse; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler to get the current function in Ghidra GUI. + * Responds with the function name, entry point, and signature. + */ +public final class GetCurrentFunction extends Handler { + /** + * Constructor for the GetCurrentFunction handler. + * + * @param tool The Ghidra PluginTool instance. + */ + public GetCurrentFunction(PluginTool tool) { + super(tool, "/get_current_function"); + } + + /** + * Handles the HTTP request to get the current function. + * + * @param exchange The HTTP exchange containing the request and response. + * @throws IOException If an I/O error occurs during handling. + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + sendResponse(exchange, getCurrentFunction()); + } + + /** + * Retrieves the current function at the current location in the Ghidra GUI. + * + * @return A string containing the function name, entry point, and signature, + * or an error message if no function is found or if there are issues. + */ + private String getCurrentFunction() { + CodeViewerService service = tool.getService(CodeViewerService.class); + if (service == null) + return "Code viewer service not available"; + + ProgramLocation location = service.getCurrentLocation(); + if (location == null) + return "No current location"; + + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + + Function func = program.getFunctionManager().getFunctionContaining(location.getAddress()); + if (func == null) + return "No function at current location: " + location.getAddress(); + + return String.format("Function: %s at %s\nSignature: %s", + func.getName(), + func.getEntryPoint(), + func.getSignature()); + } +} diff --git a/src/main/java/com/lauriewired/handlers/get/GetDataByLabel.java b/src/main/java/com/lauriewired/handlers/get/GetDataByLabel.java new file mode 100644 index 0000000..e9ecdb4 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/get/GetDataByLabel.java @@ -0,0 +1,76 @@ +package com.lauriewired.handlers.get; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.*; + +import java.io.IOException; +import java.util.*; + +import static com.lauriewired.util.ParseUtils.*; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler to retrieve data associated with a specific label in the current + * program. + * It responds with the address and value of the data defined at that label. + */ +public final class GetDataByLabel extends Handler { + /** + * Constructor for the GetDataByLabel handler. + * + * @param tool The PluginTool instance to use for accessing the current program. + */ + public GetDataByLabel(PluginTool tool) { + super(tool, "/get_data_by_label"); + } + + /** + * Handles the HTTP request to retrieve data by label. + * + * @param exchange The HttpExchange object containing the request and response. + * @throws IOException If an I/O error occurs during handling. + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + Map qparams = parseQueryParams(exchange); + String label = qparams.get("label"); + sendResponse(exchange, getDataByLabel(label)); + } + + /** + * Retrieves data associated with the specified label in the current program. + * + * @param label The label to search for in the current program. + * @return A string containing the address and value of the data defined at that + * label, + * or an error message if the label is not found or no program is + * loaded. + */ + private String getDataByLabel(String label) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + if (label == null || label.isEmpty()) + return "Label is required"; + + SymbolTable st = program.getSymbolTable(); + SymbolIterator it = st.getSymbols(label); + if (!it.hasNext()) + return "Label not found: " + label; + + StringBuilder sb = new StringBuilder(); + while (it.hasNext()) { + Symbol s = it.next(); + Address a = s.getAddress(); + Data d = program.getListing().getDefinedDataAt(a); + String v = (d != null) ? escapeNonAscii(String.valueOf(d.getDefaultValueRepresentation())) + : "(no defined data)"; + sb.append(String.format("%s -> %s : %s%n", label, a, v)); + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/lauriewired/handlers/get/GetFunctionByAddress.java b/src/main/java/com/lauriewired/handlers/get/GetFunctionByAddress.java new file mode 100644 index 0000000..40a5e30 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/get/GetFunctionByAddress.java @@ -0,0 +1,69 @@ +package com.lauriewired.handlers.get; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Program; + +import java.io.IOException; +import java.util.Map; + +import static com.lauriewired.util.ParseUtils.parseQueryParams; +import static com.lauriewired.util.ParseUtils.sendResponse; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler to get function details by address + */ +public final class GetFunctionByAddress extends Handler { + public GetFunctionByAddress(PluginTool tool) { + super(tool, "/get_function_by_address"); + } + + /** + * Handle HTTP GET request to retrieve function details by address + * + * @param exchange the HTTP exchange + * @throws IOException if an I/O error occurs + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + Map qparams = parseQueryParams(exchange); + String address = qparams.get("address"); + sendResponse(exchange, getFunctionByAddress(address)); + } + + /** + * Retrieves function details by address + * + * @param addressStr the address as a string + * @return a string containing function details or an error message + */ + private String getFunctionByAddress(String addressStr) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + if (addressStr == null || addressStr.isEmpty()) + return "Address is required"; + + try { + Address addr = program.getAddressFactory().getAddress(addressStr); + Function func = program.getFunctionManager().getFunctionAt(addr); + + if (func == null) + return "No function found at address " + addressStr; + + return String.format("Function: %s at %s\nSignature: %s\nEntry: %s\nBody: %s - %s", + func.getName(), + func.getEntryPoint(), + func.getSignature(), + func.getEntryPoint(), + func.getBody().getMinAddress(), + func.getBody().getMaxAddress()); + } catch (Exception e) { + return "Error getting function: " + e.getMessage(); + } + } +} diff --git a/src/main/java/com/lauriewired/handlers/get/GetFunctionXrefs.java b/src/main/java/com/lauriewired/handlers/get/GetFunctionXrefs.java new file mode 100644 index 0000000..4856d26 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/get/GetFunctionXrefs.java @@ -0,0 +1,91 @@ +package com.lauriewired.handlers.get; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.FunctionManager; +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.RefType; +import ghidra.program.model.symbol.Reference; +import ghidra.program.model.symbol.ReferenceIterator; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.lauriewired.util.ParseUtils.*; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler to get all references to a specific function by name. + * Expects query parameters: name, offset, limit + */ +public final class GetFunctionXrefs extends Handler { + public GetFunctionXrefs(PluginTool tool) { + super(tool, "/function_xrefs"); + } + + /** + * Handles the HTTP request to get function cross-references. + * Expects query parameters: name, offset, limit + * + * @param exchange the HTTP exchange containing the request + * @throws Exception if an error occurs while processing the request + */ + @Override + public void handle(HttpExchange exchange) throws Exception { + Map qparams = parseQueryParams(exchange); + String name = qparams.get("name"); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + sendResponse(exchange, getFunctionXrefs(name, offset, limit)); + } + + /** + * Retrieves cross-references to a function by its name. + * + * @param functionName the name of the function to find references for + * @param offset the starting index for pagination + * @param limit the maximum number of results to return + * @return a string containing the references or an error message + */ + private String getFunctionXrefs(String functionName, int offset, int limit) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + if (functionName == null || functionName.isEmpty()) + return "Function name is required"; + + try { + List refs = new ArrayList<>(); + FunctionManager funcManager = program.getFunctionManager(); + for (Function function : funcManager.getFunctions(true)) { + if (function.getName().equals(functionName)) { + Address entryPoint = function.getEntryPoint(); + ReferenceIterator refIter = program.getReferenceManager().getReferencesTo(entryPoint); + + while (refIter.hasNext()) { + Reference ref = refIter.next(); + Address fromAddr = ref.getFromAddress(); + RefType refType = ref.getReferenceType(); + + Function fromFunc = funcManager.getFunctionContaining(fromAddr); + String funcInfo = (fromFunc != null) ? " in " + fromFunc.getName() : ""; + + refs.add(String.format("From %s%s [%s]", fromAddr, funcInfo, refType.getName())); + } + } + } + + if (refs.isEmpty()) { + return "No references found to function: " + functionName; + } + + return paginateList(refs, offset, limit); + } catch (Exception e) { + return "Error getting function references: " + e.getMessage(); + } + } +} diff --git a/src/main/java/com/lauriewired/handlers/get/GetStruct.java b/src/main/java/com/lauriewired/handlers/get/GetStruct.java new file mode 100644 index 0000000..b4e5530 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/get/GetStruct.java @@ -0,0 +1,98 @@ +package com.lauriewired.handlers.get; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.data.DataType; +import ghidra.program.model.data.DataTypeComponent; +import ghidra.program.model.data.DataTypeManager; +import ghidra.program.model.data.Structure; +import ghidra.program.model.listing.Program; + +import com.google.gson.Gson; + +import java.io.IOException; +import java.util.*; + +import static com.lauriewired.util.ParseUtils.*; +import ghidra.program.model.data.CategoryPath; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler for retrieving details of a structure by its name and category. + * Expects query parameters: name (required), category (optional). + */ +public final class GetStruct extends Handler { + /** + * Constructor for the GetStruct handler. + * + * @param tool the PluginTool instance to use for accessing the current program. + */ + public GetStruct(PluginTool tool) { + super(tool, "/get_struct"); + } + + /** + * Handles the HTTP request to retrieve structure details. + * + * @param exchange the HttpExchange object containing the request and response. + * @throws IOException if an I/O error occurs during handling. + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + Map qparams = parseQueryParams(exchange); + String structName = qparams.get("name"); + String category = qparams.get("category"); + if (structName == null) { + sendResponse(exchange, "name is required"); + return; + } + sendResponse(exchange, getStruct(structName, category)); + } + + /** + * Retrieves the structure details as a JSON string. + * + * @param structName the name of the structure to retrieve. + * @param category the category path where the structure is located + * (optional). + * @return a JSON representation of the structure or an error message if not + * found. + */ + private String getStruct(String structName, String category) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + + DataTypeManager dtm = program.getDataTypeManager(); + CategoryPath path = new CategoryPath(category == null ? "/" : category); + DataType dt = dtm.getDataType(path, structName); + + if (dt == null || !(dt instanceof Structure)) { + return "Error: Struct " + structName + " not found in category " + path; + } + + Structure struct = (Structure) dt; + + Map structRepr = new HashMap<>(); + structRepr.put("name", struct.getName()); + structRepr.put("category", struct.getCategoryPath().getPath()); + structRepr.put("size", struct.getLength()); + structRepr.put("isNotYetDefined", struct.isNotYetDefined()); + + List> membersList = new ArrayList<>(); + for (DataTypeComponent component : struct.getDefinedComponents()) { + Map memberMap = new HashMap<>(); + memberMap.put("name", component.getFieldName()); + memberMap.put("type", component.getDataType().getName()); + memberMap.put("offset", component.getOffset()); + memberMap.put("size", component.getLength()); + memberMap.put("comment", component.getComment()); + membersList.add(memberMap); + } + structRepr.put("members", membersList); + + Gson gson = new Gson(); + return gson.toJson(structRepr); + } +} diff --git a/src/main/java/com/lauriewired/handlers/get/GetXrefsFrom.java b/src/main/java/com/lauriewired/handlers/get/GetXrefsFrom.java new file mode 100644 index 0000000..54c7735 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/get/GetXrefsFrom.java @@ -0,0 +1,92 @@ +package com.lauriewired.handlers.get; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Data; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.RefType; +import ghidra.program.model.symbol.Reference; +import ghidra.program.model.symbol.ReferenceManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.lauriewired.util.ParseUtils.*; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** Handler for getting cross-references from a specific address */ +public final class GetXrefsFrom extends Handler { + /** + * Constructor for the GetXrefsFrom handler. + * + * @param tool The PluginTool instance to use for accessing the current program. + */ + public GetXrefsFrom(PluginTool tool) { + super(tool, "/xrefs_from"); + } + + /** + * Handles the HTTP request to get cross-references from a specific address. + * + * @param exchange The HttpExchange object containing the request and response. + * @throws Exception If an error occurs while processing the request. + */ + @Override + public void handle(HttpExchange exchange) throws Exception { + Map qparams = parseQueryParams(exchange); + String address = qparams.get("address"); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + sendResponse(exchange, getXrefsFrom(address, offset, limit)); + } + + /** + * Get references from a specific address in the current program. + * + * @param addressStr The address to get references from. + * @param offset The offset for pagination. + * @param limit The maximum number of references to return. + * @return A string containing the references or an error message. + */ + private String getXrefsFrom(String addressStr, int offset, int limit) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + if (addressStr == null || addressStr.isEmpty()) + return "Address is required"; + + try { + Address addr = program.getAddressFactory().getAddress(addressStr); + ReferenceManager refManager = program.getReferenceManager(); + + Reference[] references = refManager.getReferencesFrom(addr); + + List refs = new ArrayList<>(); + for (Reference ref : references) { + Address toAddr = ref.getToAddress(); + RefType refType = ref.getReferenceType(); + + String targetInfo = ""; + Function toFunc = program.getFunctionManager().getFunctionAt(toAddr); + if (toFunc != null) { + targetInfo = " to function " + toFunc.getName(); + } else { + Data data = program.getListing().getDataAt(toAddr); + if (data != null) { + targetInfo = " to data " + (data.getLabel() != null ? data.getLabel() : data.getPathName()); + } + } + + refs.add(String.format("To %s%s [%s]", toAddr, targetInfo, refType.getName())); + } + + return paginateList(refs, offset, limit); + } catch (Exception e) { + return "Error getting references from address: " + e.getMessage(); + } + } +} diff --git a/src/main/java/com/lauriewired/handlers/get/GetXrefsTo.java b/src/main/java/com/lauriewired/handlers/get/GetXrefsTo.java new file mode 100644 index 0000000..a604592 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/get/GetXrefsTo.java @@ -0,0 +1,88 @@ +package com.lauriewired.handlers.get; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.RefType; +import ghidra.program.model.symbol.Reference; +import ghidra.program.model.symbol.ReferenceIterator; +import ghidra.program.model.symbol.ReferenceManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.lauriewired.util.ParseUtils.*; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler to get all references to a specific address in the current program. + * Example usage: /xrefs_to?address=0x00401000&offset=0&limit=100 + */ +public final class GetXrefsTo extends Handler { + /** + * Constructor for the GetXrefsTo handler. + * + * @param tool the Ghidra plugin tool + */ + public GetXrefsTo(PluginTool tool) { + super(tool, "/xrefs_to"); + } + + /** + * Handles the HTTP request to get cross-references to a specific address. + * + * @param exchange the HTTP exchange containing the request + * @throws Exception if an error occurs while processing the request + */ + @Override + public void handle(HttpExchange exchange) throws Exception { + Map qparams = parseQueryParams(exchange); + String address = qparams.get("address"); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + sendResponse(exchange, getXrefsTo(address, offset, limit)); + } + + /** + * Retrieves cross-references to a specific address in the current program. + * + * @param addressStr the address to get references to + * @param offset the offset for pagination + * @param limit the maximum number of results to return + * @return a string representation of the references found + */ + private String getXrefsTo(String addressStr, int offset, int limit) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + if (addressStr == null || addressStr.isEmpty()) + return "Address is required"; + + try { + Address addr = program.getAddressFactory().getAddress(addressStr); + ReferenceManager refManager = program.getReferenceManager(); + + ReferenceIterator refIter = refManager.getReferencesTo(addr); + + List refs = new ArrayList<>(); + while (refIter.hasNext()) { + Reference ref = refIter.next(); + Address fromAddr = ref.getFromAddress(); + RefType refType = ref.getReferenceType(); + + Function fromFunc = program.getFunctionManager().getFunctionContaining(fromAddr); + String funcInfo = (fromFunc != null) ? " in " + fromFunc.getName() : ""; + + refs.add(String.format("From %s%s [%s]", fromAddr, funcInfo, refType.getName())); + } + + return paginateList(refs, offset, limit); + } catch (Exception e) { + return "Error getting references to address: " + e.getMessage(); + } + } +} diff --git a/src/main/java/com/lauriewired/handlers/get/ListDefinedData.java b/src/main/java/com/lauriewired/handlers/get/ListDefinedData.java new file mode 100644 index 0000000..153c2c1 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/get/ListDefinedData.java @@ -0,0 +1,77 @@ +package com.lauriewired.handlers.get; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Data; +import ghidra.program.model.listing.DataIterator; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.MemoryBlock; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.lauriewired.util.ParseUtils.*; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler for listing defined data in the current program. + * + * Example usage: GET /data?offset=0&limit=100 + */ +public final class ListDefinedData extends Handler { + /** + * Constructs a new ListDefinedData handler. + * + * @param tool The PluginTool instance to use for accessing the current program. + */ + public ListDefinedData(PluginTool tool) { + super(tool, "/data"); + } + + /** + * Handles the HTTP request to list defined data. + * + * @param exchange The HTTP exchange containing the request. + * @throws IOException If an I/O error occurs. + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + Map qparams = parseQueryParams(exchange); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + sendResponse(exchange, listDefinedData(offset, limit)); + } + + /** + * Lists defined data in the current program, paginated by offset and limit. + * + * @param offset The starting index for pagination. + * @param limit The maximum number of items to return. + * @return A string representation of the defined data, formatted for display. + */ + private String listDefinedData(int offset, int limit) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + + List lines = new ArrayList<>(); + for (MemoryBlock block : program.getMemory().getBlocks()) { + DataIterator it = program.getListing().getDefinedData(block.getStart(), true); + while (it.hasNext()) { + Data data = it.next(); + if (block.contains(data.getAddress())) { + String label = data.getLabel() != null ? data.getLabel() : "(unnamed)"; + String valRepr = data.getDefaultValueRepresentation(); + lines.add(String.format("%s: %s = %s", + data.getAddress(), + escapeNonAscii(label), + escapeNonAscii(valRepr))); + } + } + } + return paginateList(lines, offset, limit); + } +} diff --git a/src/main/java/com/lauriewired/handlers/get/ListDefinedStrings.java b/src/main/java/com/lauriewired/handlers/get/ListDefinedStrings.java new file mode 100644 index 0000000..b279171 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/get/ListDefinedStrings.java @@ -0,0 +1,121 @@ +package com.lauriewired.handlers.get; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.data.DataType; +import ghidra.program.model.listing.Data; +import ghidra.program.model.listing.DataIterator; +import ghidra.program.model.listing.Program; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.lauriewired.util.ParseUtils.*; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler to list all defined strings in the current program + * Supports pagination and filtering by string content + */ +public final class ListDefinedStrings extends Handler { + /** + * Constructor for ListDefinedStrings handler + * + * @param tool the PluginTool instance to use for accessing the current program + */ + public ListDefinedStrings(PluginTool tool) { + super(tool, "/strings"); + } + + /** + * Handle HTTP GET requests to list defined strings + * + * @param exchange the HTTP exchange containing the request + * @throws Exception if an error occurs while processing the request + */ + @Override + public void handle(HttpExchange exchange) throws Exception { + Map qparams = parseQueryParams(exchange); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + String filter = qparams.get("filter"); + sendResponse(exchange, listDefinedStrings(offset, limit, filter)); + } + + /** + * List all defined strings in the program with their addresses + * + * @param offset the starting index for pagination + * @param limit the maximum number of results to return + * @param filter optional filter to apply to string values + * @return a formatted string containing the list of defined strings + */ + private String listDefinedStrings(int offset, int limit, String filter) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + + List lines = new ArrayList<>(); + DataIterator dataIt = program.getListing().getDefinedData(true); + + while (dataIt.hasNext()) { + Data data = dataIt.next(); + + if (data != null && isStringData(data)) { + String value = data.getValue() != null ? data.getValue().toString() : ""; + + if (filter == null || value.toLowerCase().contains(filter.toLowerCase())) { + String escapedValue = escapeString(value); + lines.add(String.format("%s: \"%s\"", data.getAddress(), escapedValue)); + } + } + } + + return paginateList(lines, offset, limit); + } + + /** + * Check if the given data is a string type + * + * @param data the Data object to check + * @return true if the data is a string type, false otherwise + */ + private boolean isStringData(Data data) { + if (data == null) + return false; + + DataType dt = data.getDataType(); + String typeName = dt.getName().toLowerCase(); + return typeName.contains("string") || typeName.contains("char") || typeName.equals("unicode"); + } + + /** + * Escape special characters in a string for safe display + * + * @param input the string to escape + * @return the escaped string + */ + private String escapeString(String input) { + if (input == null) + return ""; + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + if (c >= 32 && c < 127) { + sb.append(c); + } else if (c == '\n') { + sb.append("\\n"); + } else if (c == '\r') { + sb.append("\\r"); + } else if (c == '\t') { + sb.append("\\t"); + } else { + sb.append(String.format("\\x%02x", (int) c & 0xFF)); + } + } + return sb.toString(); + } +} diff --git a/src/main/java/com/lauriewired/handlers/get/ListExports.java b/src/main/java/com/lauriewired/handlers/get/ListExports.java new file mode 100644 index 0000000..1271c30 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/get/ListExports.java @@ -0,0 +1,75 @@ +package com.lauriewired.handlers.get; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.Symbol; +import ghidra.program.model.symbol.SymbolIterator; +import ghidra.program.model.symbol.SymbolTable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.lauriewired.util.ParseUtils.*; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler for listing all exports in the current program. + * Exports are symbols that are external entry points, typically functions. + * + * Example usage: + * GET /imports?offset=0&limit=100 + */ +public final class ListExports extends Handler { + /** + * Constructor for ListExports handler. + * + * @param tool the PluginTool instance to interact with Ghidra + */ + public ListExports(PluginTool tool) { + super(tool, "/imports"); + } + + /** + * Handles the HTTP request to list exports. + * + * @param exchange the HttpExchange instance containing the request + * @throws IOException if an I/O error occurs + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + Map qparams = parseQueryParams(exchange); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + sendResponse(exchange, listExports(offset, limit)); + } + + /** + * Lists all exports in the current program, paginated by offset and limit. + * + * @param offset the starting index for pagination + * @param limit the maximum number of exports to return + * @return a string representation of the exports, formatted for pagination + */ + private String listExports(int offset, int limit) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + + SymbolTable table = program.getSymbolTable(); + SymbolIterator it = table.getAllSymbols(true); + + List lines = new ArrayList<>(); + while (it.hasNext()) { + Symbol s = it.next(); + // On older Ghidra, "export" is recognized via isExternalEntryPoint() + if (s.isExternalEntryPoint()) { + lines.add(s.getName() + " -> " + s.getAddress()); + } + } + return paginateList(lines, offset, limit); + } +} diff --git a/src/main/java/com/lauriewired/handlers/get/ListFunctions.java b/src/main/java/com/lauriewired/handlers/get/ListFunctions.java new file mode 100644 index 0000000..ffb36df --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/get/ListFunctions.java @@ -0,0 +1,58 @@ +package com.lauriewired.handlers.get; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Program; + +import java.io.IOException; + +import static com.lauriewired.util.ParseUtils.sendResponse; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler to list all functions in the current program. + * Responds with a list of function names and their entry points. + */ +public final class ListFunctions extends Handler { + /** + * Constructor for ListFunctions handler. + * + * @param tool the PluginTool instance + */ + public ListFunctions(PluginTool tool) { + super(tool, "/list_functions"); + } + + /** + * Handles the HTTP request to list functions. + * + * @param exchange the HttpExchange instance + * @throws IOException if an I/O error occurs + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + sendResponse(exchange, listFunctions()); + } + + /** + * Lists all functions in the current program. + * + * @return a string containing the names and entry points of all functions + */ + private String listFunctions() { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + + StringBuilder result = new StringBuilder(); + for (Function func : program.getFunctionManager().getFunctions(true)) { + result.append(String.format("%s at %s\n", + func.getName(), + func.getEntryPoint())); + } + + return result.toString(); + } +} diff --git a/src/main/java/com/lauriewired/handlers/get/ListImports.java b/src/main/java/com/lauriewired/handlers/get/ListImports.java new file mode 100644 index 0000000..5e1e4f3 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/get/ListImports.java @@ -0,0 +1,66 @@ +package com.lauriewired.handlers.get; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.Symbol; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.lauriewired.util.ParseUtils.*; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler for listing all external symbols (imports) in the current program. + * Responds with a paginated list of imports in the format: + * "symbolName -> symbolAddress". + */ +public final class ListImports extends Handler { + /** + * Constructor for ListImports handler. + * + * @param tool the PluginTool instance to use for accessing the current program. + */ + public ListImports(PluginTool tool) { + super(tool, "/imports"); + } + + /** + * Handles the HTTP request to list imports. + * Expects query parameters 'offset' and 'limit' for pagination. + * + * @param exchange the HttpExchange instance containing the request and + * response. + * @throws IOException if an I/O error occurs during handling. + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + Map qparams = parseQueryParams(exchange); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + sendResponse(exchange, listImports(offset, limit)); + } + + /** + * Lists all external symbols (imports) in the current program, paginated. + * + * @param offset the starting index for pagination. + * @param limit the maximum number of results to return. + * @return a string containing the paginated list of imports. + */ + private String listImports(int offset, int limit) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + + List lines = new ArrayList<>(); + for (Symbol symbol : program.getSymbolTable().getExternalSymbols()) { + lines.add(symbol.getName() + " -> " + symbol.getAddress()); + } + return paginateList(lines, offset, limit); + } +} diff --git a/src/main/java/com/lauriewired/handlers/get/ListNamespaces.java b/src/main/java/com/lauriewired/handlers/get/ListNamespaces.java new file mode 100644 index 0000000..3a85b98 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/get/ListNamespaces.java @@ -0,0 +1,70 @@ +package com.lauriewired.handlers.get; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.GlobalNamespace; +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.Namespace; +import ghidra.program.model.symbol.Symbol; + +import java.io.IOException; +import java.util.*; + +import static com.lauriewired.util.ParseUtils.*; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler for listing namespaces in the current program. + * + * Example usage: + * GET /namespaces?offset=0&limit=100 + */ +public final class ListNamespaces extends Handler { + /** + * Constructor for the ListNamespaces handler. + * + * @param tool the PluginTool instance + */ + public ListNamespaces(PluginTool tool) { + super(tool, "/namespaces"); + } + + /** + * Handles the HTTP request to list namespaces. + * + * @param exchange the HttpExchange instance containing the request + * @throws IOException if an I/O error occurs + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + Map qparams = parseQueryParams(exchange); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + sendResponse(exchange, listNamespaces(offset, limit)); + } + + /** + * Lists namespaces in the current program, paginated by offset and limit. + * + * @param offset the starting index for pagination + * @param limit the maximum number of namespaces to return + * @return a string representation of the paginated list of namespaces + */ + private String listNamespaces(int offset, int limit) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + + Set namespaces = new HashSet<>(); + for (Symbol symbol : program.getSymbolTable().getAllSymbols(true)) { + Namespace ns = symbol.getParentNamespace(); + if (ns != null && !(ns instanceof GlobalNamespace)) { + namespaces.add(ns.getName()); + } + } + List sorted = new ArrayList<>(namespaces); + Collections.sort(sorted); + return paginateList(sorted, offset, limit); + } +} diff --git a/src/main/java/com/lauriewired/handlers/get/ListSegments.java b/src/main/java/com/lauriewired/handlers/get/ListSegments.java new file mode 100644 index 0000000..7da65ee --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/get/ListSegments.java @@ -0,0 +1,66 @@ +package com.lauriewired.handlers.get; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.MemoryBlock; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.lauriewired.util.ParseUtils.*; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler for listing memory segments in the current program. + * Responds with a list of memory blocks, paginated by offset and limit. + */ +public final class ListSegments extends Handler { + /** + * Constructor for ListSegments handler. + * + * @param tool the PluginTool instance to use for accessing the current program. + */ + public ListSegments(PluginTool tool) { + super(tool, "/segments"); + } + + /** + * Handles the HTTP request to list memory segments. + * Expects query parameters 'offset' and 'limit' for pagination. + * + * @param exchange the HttpExchange instance containing the request and + * response. + * @throws IOException if an I/O error occurs during handling. + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + Map qparams = parseQueryParams(exchange); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + sendResponse(exchange, listSegments(offset, limit)); + } + + /** + * Lists memory segments in the current program, paginated by offset and limit. + * + * @param offset the starting index for pagination. + * @param limit the maximum number of segments to return. + * @return a string representation of the memory segments, formatted for + * pagination. + */ + private String listSegments(int offset, int limit) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + + List lines = new ArrayList<>(); + for (MemoryBlock block : program.getMemory().getBlocks()) { + lines.add(String.format("%s: %s - %s", block.getName(), block.getStart(), block.getEnd())); + } + return paginateList(lines, offset, limit); + } +} diff --git a/src/main/java/com/lauriewired/handlers/search/SearchBytes.java b/src/main/java/com/lauriewired/handlers/search/SearchBytes.java new file mode 100644 index 0000000..f0d08c7 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/search/SearchBytes.java @@ -0,0 +1,84 @@ +package com.lauriewired.handlers.search; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; +import ghidra.program.model.mem.Memory; +import ghidra.util.task.TaskMonitorAdapter; + +import java.io.IOException; +import java.util.*; + +import static com.lauriewired.util.ParseUtils.*; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler for searching for byte sequences in the current program's memory. + * Expects a hex string of bytes to search for, with optional pagination + * parameters. + */ +public final class SearchBytes extends Handler { + /** + * Constructor for the SearchBytes handler. + * + * @param tool The PluginTool instance to use. + */ + public SearchBytes(PluginTool tool) { + super(tool, "/search_bytes"); + } + + /** + * Parses the query parameters from the HTTP request. + * + * @param exchange The HttpExchange containing the request. + * @return A map of query parameters. + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + Map qparams = parseQueryParams(exchange); + String bytesHex = qparams.get("bytes"); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + sendResponse(exchange, searchBytes(bytesHex, offset, limit)); + } + + /** + * Searches for the specified byte sequence in the current program's memory. + * + * @param bytesHex The hex string of bytes to search for. + * @param offset The starting index for pagination. + * @param limit The maximum number of results to return. + * @return A string containing the search results, formatted for pagination. + */ + private String searchBytes(String bytesHex, int offset, int limit) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + if (bytesHex == null || bytesHex.isEmpty()) + return "Byte sequence required"; + + byte[] needle; + try { + needle = decodeHex(bytesHex); + } catch (IllegalArgumentException e) { + return "Invalid hex string: " + bytesHex; + } + + Memory mem = program.getMemory(); + List hits = new ArrayList<>(); + + Address cur = mem.getMinAddress(); + while (cur != null && hits.size() < offset + limit) { + Address found = mem.findBytes(cur, needle, null, true, TaskMonitorAdapter.DUMMY_MONITOR); + if (found == null) + break; + hits.add(found.toString()); + + cur = found.add(1); + } + + return paginateList(hits, offset, limit); + } +} \ No newline at end of file diff --git a/src/main/java/com/lauriewired/handlers/search/SearchFunctions.java b/src/main/java/com/lauriewired/handlers/search/SearchFunctions.java new file mode 100644 index 0000000..d4fc855 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/search/SearchFunctions.java @@ -0,0 +1,83 @@ +package com.lauriewired.handlers.search; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Program; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static com.lauriewired.util.ParseUtils.*; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler for searching functions by name in the current program. + * Expects query parameters: query (search term), offset, limit. + */ +public final class SearchFunctions extends Handler { + /** + * Constructor for SearchFunctions handler. + * + * @param tool the PluginTool instance to use for accessing the current program. + */ + public SearchFunctions(PluginTool tool) { + super(tool, "/searchFunctions"); + } + + /** + * Handles HTTP GET requests to search for functions by name. + * Expects query parameters: + * - query: the search term (required) + * - offset: pagination offset (default 0) + * - limit: maximum number of results to return (default 100) + * + * @param exchange the HttpExchange object containing the request and response. + * @throws IOException if an I/O error occurs. + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + Map qparams = parseQueryParams(exchange); + String searchTerm = qparams.get("query"); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + sendResponse(exchange, searchFunctionsByName(searchTerm, offset, limit)); + } + + /** + * Searches for functions in the current program by name. + * Returns a paginated list of matching functions. + * + * @param searchTerm the term to search for in function names. + * @param offset the pagination offset. + * @param limit the maximum number of results to return. + * @return a string containing the results or an error message. + */ + private String searchFunctionsByName(String searchTerm, int offset, int limit) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + if (searchTerm == null || searchTerm.isEmpty()) + return "Search term is required"; + + List matches = new ArrayList<>(); + for (Function func : program.getFunctionManager().getFunctions(true)) { + String name = func.getName(); + // simple substring match + if (name.toLowerCase().contains(searchTerm.toLowerCase())) { + matches.add(String.format("%s @ %s", name, func.getEntryPoint())); + } + } + + Collections.sort(matches); + + if (matches.isEmpty()) { + return "No functions matching '" + searchTerm + "'"; + } + return paginateList(matches, offset, limit); + } +} diff --git a/src/main/java/com/lauriewired/handlers/set/ClearStruct.java b/src/main/java/com/lauriewired/handlers/set/ClearStruct.java new file mode 100644 index 0000000..cdb26f1 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/set/ClearStruct.java @@ -0,0 +1,100 @@ +package com.lauriewired.handlers.set; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.data.DataType; +import ghidra.program.model.data.DataTypeManager; +import ghidra.program.model.data.Structure; +import ghidra.program.model.listing.Program; + +import javax.swing.SwingUtilities; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; + +import static com.lauriewired.util.ParseUtils.*; +import ghidra.program.model.data.CategoryPath; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler for clearing the contents of a structure in Ghidra. + * This handler processes requests to clear a specified structure by name and + * category. + */ +public final class ClearStruct extends Handler { + /** + * Constructs a new ClearStruct handler. + * + * @param tool the PluginTool instance to use for program operations + */ + public ClearStruct(PluginTool tool) { + super(tool, "/clear_struct"); + } + + /** + * Handles HTTP requests to clear a structure. + * Expects POST parameters: struct_name (required), category (optional). + * + * @param exchange the HttpExchange object containing the request + * @throws IOException if an I/O error occurs + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + Map params = parsePostParams(exchange); + String structName = params.get("struct_name"); + String category = params.get("category"); + if (structName == null) { + sendResponse(exchange, "struct_name is required"); + return; + } + sendResponse(exchange, clearStruct(structName, category)); + } + + /** + * Clears the contents of a structure. + * + * @param structName the name of the structure to clear + * @param category the category of the structure + * @return a message indicating the result of the operation + */ + private String clearStruct(String structName, String category) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + + final AtomicReference result = new AtomicReference<>(); + try { + SwingUtilities.invokeAndWait(() -> { + int txId = program.startTransaction("Clear Struct"); + boolean success = false; + try { + DataTypeManager dtm = program.getDataTypeManager(); + CategoryPath path = new CategoryPath(category == null ? "/" : category); + DataType dt = dtm.getDataType(path, structName); + if (dt == null || !(dt instanceof Structure)) { + result.set("Error: Struct " + structName + " not found in category " + path); + return; + } + Structure struct = (Structure) dt; + if (struct.isNotYetDefined()) { + result.set("Struct " + structName + " is empty, nothing to clear."); + success = true; // Not an error state + return; + } + struct.deleteAll(); + result.set("Struct " + structName + " cleared."); + success = true; + } catch (Exception e) { + result.set("Error: Failed to clear struct: " + e.getMessage()); + } finally { + program.endTransaction(txId, success); + } + }); + } catch (InterruptedException | InvocationTargetException e) { + return "Error: Failed to execute clear struct on Swing thread: " + e.getMessage(); + } + return result.get(); + } +} diff --git a/src/main/java/com/lauriewired/handlers/set/RenameData.java b/src/main/java/com/lauriewired/handlers/set/RenameData.java new file mode 100644 index 0000000..4189791 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/set/RenameData.java @@ -0,0 +1,91 @@ +package com.lauriewired.handlers.set; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Data; +import ghidra.program.model.listing.Listing; +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.SourceType; +import ghidra.program.model.symbol.Symbol; +import ghidra.program.model.symbol.SymbolTable; +import ghidra.util.Msg; + +import javax.swing.*; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.Map; + +import static com.lauriewired.util.ParseUtils.parsePostParams; +import static com.lauriewired.util.ParseUtils.sendResponse; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler for renaming data at a specific address in the current program. + * Expects POST parameters: "address" (the address of the data) and "newName" + * (the new name). + */ +public final class RenameData extends Handler { + /** + * Constructs a new RenameData handler. + * + * @param tool the PluginTool instance to use for program access + */ + public RenameData(PluginTool tool) { + super(tool, "/renameData"); + } + + /** + * Handles the HTTP request to rename data at a specified address. + * Expects POST parameters "address" and "newName". + * + * @param exchange the HttpExchange object containing the request + * @throws IOException if an I/O error occurs + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + Map params = parsePostParams(exchange); + renameDataAtAddress(params.get("address"), params.get("newName")); + sendResponse(exchange, "Rename data attempted"); + } + + /** + * Renames the data at the specified address in the current program. + * If the data exists, it updates its name; otherwise, it creates a new label. + * + * @param addressStr the address of the data as a string + * @param newName the new name for the data + */ + private void renameDataAtAddress(String addressStr, String newName) { + Program program = getCurrentProgram(tool); + if (program == null) + return; + + try { + SwingUtilities.invokeAndWait(() -> { + int tx = program.startTransaction("Rename data"); + try { + Address addr = program.getAddressFactory().getAddress(addressStr); + Listing listing = program.getListing(); + Data data = listing.getDefinedDataAt(addr); + if (data != null) { + SymbolTable symTable = program.getSymbolTable(); + Symbol symbol = symTable.getPrimarySymbol(addr); + if (symbol != null) { + symbol.setName(newName, SourceType.USER_DEFINED); + } else { + symTable.createLabel(addr, newName, SourceType.USER_DEFINED); + } + } + } catch (Exception e) { + Msg.error(this, "Rename data error", e); + } finally { + program.endTransaction(tx, true); + } + }); + } catch (InterruptedException | InvocationTargetException e) { + Msg.error(this, "Failed to execute rename data on Swing thread", e); + } + } +} diff --git a/src/main/java/com/lauriewired/handlers/set/RenameFunction.java b/src/main/java/com/lauriewired/handlers/set/RenameFunction.java new file mode 100644 index 0000000..08025e3 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/set/RenameFunction.java @@ -0,0 +1,86 @@ +package com.lauriewired.handlers.set; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.SourceType; +import ghidra.util.Msg; + +import javax.swing.*; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.lauriewired.util.ParseUtils.parsePostParams; +import static com.lauriewired.util.ParseUtils.sendResponse; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler for renaming a function in the current program. + * Expects POST parameters: oldName and newName. + */ +public final class RenameFunction extends Handler { + /** + * Constructor for RenameFunction handler. + * + * @param tool the PluginTool instance to interact with Ghidra + */ + public RenameFunction(PluginTool tool) { + super(tool, "/renameFunction"); + } + + /** + * Handles the HTTP request to rename a function. + * Expects parameters "oldName" and "newName" in the POST request. + * + * @param exchange the HttpExchange object containing the request + * @throws IOException if an I/O error occurs + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + Map params = parsePostParams(exchange); + String response = rename(params.get("oldName"), params.get("newName")) + ? "Renamed successfully" + : "Rename failed"; + sendResponse(exchange, response); + } + + /** + * Renames a function in the current program. + * + * @param oldName the current name of the function + * @param newName the new name to set for the function + * @return true if the rename was successful, false otherwise + */ + private boolean rename(String oldName, String newName) { + Program program = getCurrentProgram(tool); + if (program == null) + return false; + + AtomicBoolean successFlag = new AtomicBoolean(false); + try { + SwingUtilities.invokeAndWait(() -> { + int tx = program.startTransaction("Rename function via HTTP"); + try { + for (Function func : program.getFunctionManager().getFunctions(true)) { + if (func.getName().equals(oldName)) { + func.setName(newName, SourceType.USER_DEFINED); + successFlag.set(true); + break; + } + } + } catch (Exception e) { + Msg.error(this, "Error renaming function", e); + } finally { + program.endTransaction(tx, successFlag.get()); + } + }); + } catch (InterruptedException | InvocationTargetException e) { + Msg.error(this, "Failed to execute rename on Swing thread", e); + } + return successFlag.get(); + } +} diff --git a/src/main/java/com/lauriewired/handlers/set/RenameFunctionByAddress.java b/src/main/java/com/lauriewired/handlers/set/RenameFunctionByAddress.java new file mode 100644 index 0000000..bcbaac2 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/set/RenameFunctionByAddress.java @@ -0,0 +1,105 @@ +package com.lauriewired.handlers.set; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.SourceType; +import ghidra.util.Msg; + +import javax.swing.*; +import java.lang.reflect.InvocationTargetException; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.lauriewired.util.ParseUtils.parsePostParams; +import static com.lauriewired.util.ParseUtils.sendResponse; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler to rename a function by its address + */ +public final class RenameFunctionByAddress extends Handler { + /** + * Constructor for the RenameFunctionByAddress handler + * + * @param tool the PluginTool instance + */ + public RenameFunctionByAddress(PluginTool tool) { + super(tool, "/rename_function_by_address"); + } + + /** + * Handle the HTTP request to rename a function by its address + * + * @param exchange the HttpExchange instance containing the request + * @throws Exception if an error occurs during processing + */ + @Override + public void handle(HttpExchange exchange) throws Exception { + Map params = parsePostParams(exchange); + String functionAddress = params.get("function_address"); + String newName = params.get("new_name"); + boolean success = renameFunctionByAddress(functionAddress, newName); + sendResponse(exchange, success ? "Function renamed successfully" : "Failed to rename function"); + } + + /** + * Renames a function by its address + * + * @param functionAddrStr the address of the function as a string + * @param newName the new name for the function + * @return true if the rename was successful, false otherwise + */ + private boolean renameFunctionByAddress(String functionAddrStr, String newName) { + Program program = getCurrentProgram(tool); + if (program == null) + return false; + if (functionAddrStr == null || functionAddrStr.isEmpty() || + newName == null || newName.isEmpty()) { + return false; + } + + AtomicBoolean success = new AtomicBoolean(false); + + try { + SwingUtilities.invokeAndWait(() -> { + performFunctionRename(program, functionAddrStr, newName, success); + }); + } catch (InterruptedException | InvocationTargetException e) { + Msg.error(this, "Failed to execute rename function on Swing thread", e); + } + + return success.get(); + } + + /** + * Performs the function renaming operation on the Swing thread + * + * @param program the current program + * @param functionAddrStr the address of the function as a string + * @param newName the new name for the function + * @param success an AtomicBoolean to indicate success or failure + */ + private void performFunctionRename(Program program, String functionAddrStr, String newName, AtomicBoolean success) { + int tx = program.startTransaction("Rename function by address"); + try { + Address addr = program.getAddressFactory().getAddress(functionAddrStr); + Function func = program.getListing().getFunctionContaining(addr); + + if (func == null) { + Msg.error(this, "Could not find function at address: " + functionAddrStr); + return; + } + + func.setName(newName, SourceType.USER_DEFINED); + success.set(true); + } catch (Exception e) { + Msg.error(this, "Error renaming function by address", e); + } finally { + program.endTransaction(tx, success.get()); + } + } +} diff --git a/src/main/java/com/lauriewired/handlers/set/RenameVariable.java b/src/main/java/com/lauriewired/handlers/set/RenameVariable.java new file mode 100644 index 0000000..6e28ed0 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/set/RenameVariable.java @@ -0,0 +1,191 @@ +package com.lauriewired.handlers.set; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.app.decompiler.DecompInterface; +import ghidra.app.decompiler.DecompileResults; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Parameter; +import ghidra.program.model.listing.Program; +import ghidra.program.model.listing.VariableStorage; +import ghidra.program.model.pcode.HighFunction; +import ghidra.program.model.pcode.HighFunctionDBUtil; +import ghidra.program.model.pcode.HighSymbol; +import ghidra.program.model.pcode.LocalSymbolMap; +import ghidra.program.model.symbol.SourceType; +import ghidra.util.Msg; +import ghidra.util.task.ConsoleTaskMonitor; + +import javax.swing.*; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.lauriewired.util.ParseUtils.parsePostParams; +import static com.lauriewired.util.ParseUtils.sendResponse; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler for renaming a variable in a function. + * Expects POST parameters: functionName, oldName, newName. + * Returns a message indicating success or failure. + */ +public final class RenameVariable extends Handler { + /** + * Constructor for the RenameVariable handler. + * + * @param tool the PluginTool instance + */ + public RenameVariable(PluginTool tool) { + super(tool, "/renameVariable"); + } + + /** + * Handles the HTTP request to rename a variable in a function. + * + * @param exchange the HttpExchange object containing the request + * @throws IOException if an I/O error occurs + */ + @Override + public void handle(HttpExchange exchange) throws IOException { + Map params = parsePostParams(exchange); + String functionName = params.get("functionName"); + String oldName = params.get("oldName"); + String newName = params.get("newName"); + String result = renameVariableInFunction(functionName, oldName, newName); + sendResponse(exchange, result); + } + + /** + * Renames a variable in the specified function. + * + * @param functionName the name of the function containing the variable + * @param oldVarName the current name of the variable to rename + * @param newVarName the new name for the variable + * @return a message indicating success or failure + */ + private String renameVariableInFunction(String functionName, String oldVarName, String newVarName) { + Program program = getCurrentProgram(tool); + if (program == null) + return "No program loaded"; + + DecompInterface decomp = new DecompInterface(); + decomp.openProgram(program); + + Function func = null; + for (Function f : program.getFunctionManager().getFunctions(true)) { + if (f.getName().equals(functionName)) { + func = f; + break; + } + } + + if (func == null) { + return "Function not found"; + } + + DecompileResults result = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor()); + if (result == null || !result.decompileCompleted()) { + return "Decompilation failed"; + } + + HighFunction highFunction = result.getHighFunction(); + if (highFunction == null) { + return "Decompilation failed (no high function)"; + } + + LocalSymbolMap localSymbolMap = highFunction.getLocalSymbolMap(); + if (localSymbolMap == null) { + return "Decompilation failed (no local symbol map)"; + } + + HighSymbol highSymbol = null; + Iterator symbols = localSymbolMap.getSymbols(); + while (symbols.hasNext()) { + HighSymbol symbol = symbols.next(); + String symbolName = symbol.getName(); + + if (symbolName.equals(oldVarName)) { + highSymbol = symbol; + } + if (symbolName.equals(newVarName)) { + return "Error: A variable with name '" + newVarName + "' already exists in this function"; + } + } + + if (highSymbol == null) { + return "Variable not found"; + } + + boolean commitRequired = checkFullCommit(highSymbol, highFunction); + + final HighSymbol finalHighSymbol = highSymbol; + final Function finalFunction = func; + AtomicBoolean successFlag = new AtomicBoolean(false); + + try { + SwingUtilities.invokeAndWait(() -> { + int tx = program.startTransaction("Rename variable"); + try { + if (commitRequired) { + HighFunctionDBUtil.commitParamsToDatabase(highFunction, false, + HighFunctionDBUtil.ReturnCommitOption.NO_COMMIT, finalFunction.getSignatureSource()); + } + HighFunctionDBUtil.updateDBVariable( + finalHighSymbol, + newVarName, + null, + SourceType.USER_DEFINED); + successFlag.set(true); + } catch (Exception e) { + Msg.error(this, "Failed to rename variable", e); + } finally { + program.endTransaction(tx, true); + } + }); + } catch (InterruptedException | InvocationTargetException e) { + String errorMsg = "Failed to execute rename on Swing thread: " + e.getMessage(); + Msg.error(this, errorMsg, e); + return errorMsg; + } + return successFlag.get() ? "Variable renamed" : "Failed to rename variable"; + } + + /** + * Checks if a full commit is required for the variable renaming operation. + * + * @param highSymbol the HighSymbol representing the variable + * @param hfunction the HighFunction containing the variable + * @return true if a full commit is required, false otherwise + */ + protected static boolean checkFullCommit(HighSymbol highSymbol, HighFunction hfunction) { + if (highSymbol != null && !highSymbol.isParameter()) { + return false; + } + Function function = hfunction.getFunction(); + Parameter[] parameters = function.getParameters(); + LocalSymbolMap localSymbolMap = hfunction.getLocalSymbolMap(); + int numParams = localSymbolMap.getNumParams(); + if (numParams != parameters.length) { + return true; + } + + for (int i = 0; i < numParams; i++) { + HighSymbol param = localSymbolMap.getParamSymbol(i); + if (param.getCategoryIndex() != i) { + return true; + } + VariableStorage storage = param.getStorage(); + // Don't compare using the equals method so that DynamicVariableStorage can + // match + if (0 != storage.compareTo(parameters[i].getVariableStorage())) { + return true; + } + } + + return false; + } +} diff --git a/src/main/java/com/lauriewired/handlers/set/SetFunctionPrototype.java b/src/main/java/com/lauriewired/handlers/set/SetFunctionPrototype.java new file mode 100644 index 0000000..3e4616f --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/set/SetFunctionPrototype.java @@ -0,0 +1,255 @@ +package com.lauriewired.handlers.set; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.data.DataTypeManager; +import ghidra.program.model.listing.CodeUnit; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.SourceType; +import ghidra.util.Msg; +import ghidra.util.task.ConsoleTaskMonitor; + +import javax.swing.*; +import java.lang.reflect.InvocationTargetException; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.lauriewired.util.ParseUtils.parsePostParams; +import static com.lauriewired.util.ParseUtils.sendResponse; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler to set a function prototype in Ghidra + * This handler processes HTTP requests to set a function prototype based on the + * provided address and prototype string. + */ +public final class SetFunctionPrototype extends Handler { + /** + * Constructor for the SetFunctionPrototype handler + * + * @param tool The Ghidra plugin tool instance + */ + public SetFunctionPrototype(PluginTool tool) { + super(tool, "/set_function_prototype"); + } + + /** + * Result class to encapsulate success/failure and error messages + */ + private static class PrototypeResult { + /** Indicates if the prototype was set successfully */ + private final boolean success; + + /** Detailed error message if the operation failed */ + private final String errorMessage; + + /** + * Constructor for PrototypeResult + * + * @param success Indicates if the operation was successful + * @param errorMessage Detailed error message if applicable + */ + public PrototypeResult(boolean success, String errorMessage) { + this.success = success; + this.errorMessage = errorMessage; + } + + /** + * Getters for success status and error message + */ + public boolean isSuccess() { + return success; + } + + /** + * Get the error message if the operation failed + * + * @return Error message or empty string if successful + */ + public String getErrorMessage() { + return errorMessage; + } + } + + /** + * Handle the HTTP request to set a function prototype + * + * @param exchange The HTTP exchange containing the request and response + * @throws Exception If an error occurs during processing + */ + @Override + public void handle(HttpExchange exchange) throws Exception { + Map params = parsePostParams(exchange); + String functionAddress = params.get("function_address"); + String prototype = params.get("prototype"); + + // Call the set prototype function and get detailed result + PrototypeResult result = setFunctionPrototype(functionAddress, prototype); + + if (result.isSuccess()) { + // Even with successful operations, include any warning messages for debugging + String successMsg = "Function prototype set successfully"; + if (!result.getErrorMessage().isEmpty()) { + successMsg += "\n\nWarnings/Debug Info:\n" + result.getErrorMessage(); + } + sendResponse(exchange, successMsg); + } else { + // Return the detailed error message to the client + sendResponse(exchange, "Failed to set function prototype: " + result.getErrorMessage()); + } + } + + /** + * Set the function prototype for a given function address + * + * @param functionAddrStr The address of the function as a string + * @param prototype The prototype string to set + * @return PrototypeResult indicating success or failure with error message + */ + private PrototypeResult setFunctionPrototype(String functionAddrStr, String prototype) { + // Input validation + Program program = getCurrentProgram(tool); + if (program == null) + return new PrototypeResult(false, "No program loaded"); + if (functionAddrStr == null || functionAddrStr.isEmpty()) { + return new PrototypeResult(false, "Function address is required"); + } + if (prototype == null || prototype.isEmpty()) { + return new PrototypeResult(false, "Function prototype is required"); + } + + final StringBuilder errorMessage = new StringBuilder(); + final AtomicBoolean success = new AtomicBoolean(false); + + try { + SwingUtilities.invokeAndWait( + () -> applyFunctionPrototype(program, functionAddrStr, prototype, success, errorMessage)); + } catch (InterruptedException | InvocationTargetException e) { + String msg = "Failed to set function prototype on Swing thread: " + e.getMessage(); + errorMessage.append(msg); + Msg.error(this, msg, e); + } + + return new PrototypeResult(success.get(), errorMessage.toString()); + } + + /** + * Apply the function prototype in a Swing thread to avoid blocking the UI + * + * @param program The current program + * @param functionAddrStr The address of the function as a string + * @param prototype The prototype string to set + * @param success Atomic boolean to indicate success + * @param errorMessage StringBuilder to collect error messages + */ + private void applyFunctionPrototype(Program program, String functionAddrStr, String prototype, + AtomicBoolean success, StringBuilder errorMessage) { + try { + // Get the address and function + Address addr = program.getAddressFactory().getAddress(functionAddrStr); + Function func = program.getListing().getFunctionContaining(addr); + + if (func == null) { + String msg = "Could not find function at address: " + functionAddrStr; + errorMessage.append(msg); + Msg.error(this, msg); + return; + } + + Msg.info(this, "Setting prototype for function " + func.getName() + ": " + prototype); + + // Store original prototype as a comment for reference + addPrototypeComment(program, func, prototype); + + // Use ApplyFunctionSignatureCmd to parse and apply the signature + parseFunctionSignatureAndApply(program, addr, prototype, success, errorMessage); + + } catch (Exception e) { + String msg = "Error setting function prototype: " + e.getMessage(); + errorMessage.append(msg); + Msg.error(this, msg, e); + } + } + + /** + * Add a comment to the function indicating the prototype being set + * + * @param program The current program + * @param func The function to add the comment to + * @param prototype The prototype string being set + */ + private void addPrototypeComment(Program program, Function func, String prototype) { + int txComment = program.startTransaction("Add prototype comment"); + try { + program.getListing().setComment( + func.getEntryPoint(), + CodeUnit.PLATE_COMMENT, + "Setting prototype: " + prototype); + } finally { + program.endTransaction(txComment, true); + } + } + + /** + * Parse the function signature from the prototype string and apply it to the + * function + * + * @param program The current program + * @param addr The address of the function + * @param prototype The prototype string to set + * @param success Atomic boolean to indicate success + * @param errorMessage StringBuilder to collect error messages + */ + private void parseFunctionSignatureAndApply(Program program, Address addr, String prototype, + AtomicBoolean success, StringBuilder errorMessage) { + // Use ApplyFunctionSignatureCmd to parse and apply the signature + int txProto = program.startTransaction("Set function prototype"); + try { + // Get data type manager + DataTypeManager dtm = program.getDataTypeManager(); + + // Get data type manager service + ghidra.app.services.DataTypeManagerService dtms = tool + .getService(ghidra.app.services.DataTypeManagerService.class); + + // Create function signature parser + ghidra.app.util.parser.FunctionSignatureParser parser = new ghidra.app.util.parser.FunctionSignatureParser( + dtm, dtms); + + // Parse the prototype into a function signature + ghidra.program.model.data.FunctionDefinitionDataType sig = parser.parse(null, prototype); + + if (sig == null) { + String msg = "Failed to parse function prototype"; + errorMessage.append(msg); + Msg.error(this, msg); + return; + } + + // Create and apply the command + ghidra.app.cmd.function.ApplyFunctionSignatureCmd cmd = new ghidra.app.cmd.function.ApplyFunctionSignatureCmd( + addr, sig, SourceType.USER_DEFINED); + + // Apply the command to the program + boolean cmdResult = cmd.applyTo(program, new ConsoleTaskMonitor()); + + if (cmdResult) { + success.set(true); + Msg.info(this, "Successfully applied function signature"); + } else { + String msg = "Command failed: " + cmd.getStatusMsg(); + errorMessage.append(msg); + Msg.error(this, msg); + } + } catch (Exception e) { + String msg = "Error applying function signature: " + e.getMessage(); + errorMessage.append(msg); + Msg.error(this, msg, e); + } finally { + program.endTransaction(txProto, success.get()); + } + } +} diff --git a/src/main/java/com/lauriewired/handlers/set/SetLocalVariableType.java b/src/main/java/com/lauriewired/handlers/set/SetLocalVariableType.java new file mode 100644 index 0000000..5a46375 --- /dev/null +++ b/src/main/java/com/lauriewired/handlers/set/SetLocalVariableType.java @@ -0,0 +1,308 @@ +package com.lauriewired.handlers.set; + +import com.lauriewired.handlers.Handler; +import com.sun.net.httpserver.HttpExchange; +import ghidra.app.decompiler.DecompInterface; +import ghidra.app.decompiler.DecompileResults; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.address.Address; +import ghidra.program.model.data.DataType; +import ghidra.program.model.data.DataTypeManager; +import ghidra.program.model.data.PointerDataType; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.Program; +import ghidra.program.model.pcode.HighFunctionDBUtil; +import ghidra.program.model.pcode.HighSymbol; +import ghidra.program.model.pcode.HighVariable; +import ghidra.program.model.symbol.SourceType; +import ghidra.util.Msg; +import ghidra.util.task.ConsoleTaskMonitor; + +import javax.swing.*; +import java.lang.reflect.InvocationTargetException; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.lauriewired.util.GhidraUtils.resolveDataType; +import static com.lauriewired.util.ParseUtils.*; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; + +/** + * Handler for setting the type of a local variable in a function. + * This handler allows users to specify a function address, variable name, + * and the new type they want to set for that variable. + */ +public final class SetLocalVariableType extends Handler { + /** + * Constructor for the SetLocalVariableType handler. + * + * @param tool The PluginTool instance to use for accessing the current program. + */ + public SetLocalVariableType(PluginTool tool) { + super(tool, "/set_local_variable_type"); + } + + /** + * Handles the HTTP request to set a local variable's type. + * + * @param exchange The HttpExchange object containing the request and response. + * @throws Exception If an error occurs while processing the request. + */ + @Override + public void handle(HttpExchange exchange) throws Exception { + Map params = parsePostParams(exchange); + String functionAddress = params.get("function_address"); + String variableName = params.get("variable_name"); + String newType = params.get("new_type"); + + // Capture detailed information about setting the type + StringBuilder responseMsg = new StringBuilder(); + responseMsg.append("Setting variable type: ").append(variableName) + .append(" to ").append(newType) + .append(" in function at ").append(functionAddress).append("\n\n"); + + // Attempt to find the data type in various categories + Program program = getCurrentProgram(tool); + if (program != null) { + DataTypeManager dtm = program.getDataTypeManager(); + DataType directType = findDataTypeByNameInAllCategories(dtm, newType); + if (directType != null) { + responseMsg.append("Found type: ").append(directType.getPathName()).append("\n"); + } else if (newType.startsWith("P") && newType.length() > 1) { + String baseTypeName = newType.substring(1); + DataType baseType = findDataTypeByNameInAllCategories(dtm, baseTypeName); + if (baseType != null) { + responseMsg.append("Found base type for pointer: ").append(baseType.getPathName()).append("\n"); + } else { + responseMsg.append("Base type not found for pointer: ").append(baseTypeName).append("\n"); + } + } else { + responseMsg.append("Type not found directly: ").append(newType).append("\n"); + } + } + + // Try to set the type + boolean success = setLocalVariableType(functionAddress, variableName, newType); + + String successMsg = success ? "Variable type set successfully" : "Failed to set variable type"; + responseMsg.append("\nResult: ").append(successMsg); + + sendResponse(exchange, responseMsg.toString()); + } + + /** + * Sets the type of a local variable in a function. + * + * @param functionAddrStr The address of the function as a string. + * @param variableName The name of the variable to change. + * @param newType The new type to set for the variable. + * @return true if the type was set successfully, false otherwise. + */ + private boolean setLocalVariableType(String functionAddrStr, String variableName, String newType) { + // Input validation + Program program = getCurrentProgram(tool); + if (program == null) + return false; + if (functionAddrStr == null || functionAddrStr.isEmpty() || + variableName == null || variableName.isEmpty() || + newType == null || newType.isEmpty()) { + return false; + } + + AtomicBoolean success = new AtomicBoolean(false); + + try { + SwingUtilities + .invokeAndWait(() -> applyVariableType(program, functionAddrStr, variableName, newType, success)); + } catch (InterruptedException | InvocationTargetException e) { + Msg.error(this, "Failed to execute set variable type on Swing thread", e); + } + + return success.get(); + } + + /** + * Applies the new type to the specified variable in the given function. + * This method is run on the Swing thread to ensure UI updates are safe. + * + * @param program The current program. + * @param functionAddrStr The address of the function as a string. + * @param variableName The name of the variable to change. + * @param newType The new type to set for the variable. + * @param success AtomicBoolean to indicate if the operation was + * successful. + */ + private void applyVariableType(Program program, String functionAddrStr, + String variableName, String newType, AtomicBoolean success) { + try { + // Find the function + Address addr = program.getAddressFactory().getAddress(functionAddrStr); + Function func = program.getListing().getFunctionContaining(addr); + + if (func == null) { + Msg.error(this, "Could not find function at address: " + functionAddrStr); + return; + } + + DecompileResults results = decompileFunction(func, program); + if (results == null || !results.decompileCompleted()) { + return; + } + + ghidra.program.model.pcode.HighFunction highFunction = results.getHighFunction(); + if (highFunction == null) { + Msg.error(this, "No high function available"); + return; + } + + // Find the symbol by name + HighSymbol symbol = findSymbolByName(highFunction, variableName); + if (symbol == null) { + Msg.error(this, "Could not find variable '" + variableName + "' in decompiled function"); + return; + } + + // Get high variable + HighVariable highVar = symbol.getHighVariable(); + if (highVar == null) { + Msg.error(this, "No HighVariable found for symbol: " + variableName); + return; + } + + Msg.info(this, "Found high variable for: " + variableName + + " with current type " + highVar.getDataType().getName()); + + // Find the data type + DataTypeManager dtm = program.getDataTypeManager(); + DataType dataType = resolveDataType(tool, dtm, newType); + + if (dataType == null) { + Msg.error(this, "Could not resolve data type: " + newType); + return; + } + + Msg.info(this, "Using data type: " + dataType.getName() + " for variable " + variableName); + + // Apply the type change in a transaction + updateVariableType(program, symbol, dataType, success); + + } catch (Exception e) { + Msg.error(this, "Error setting variable type: " + e.getMessage()); + } + } + + /** + * Helper method to find a data type by name in all categories, handling case + * sensitivity. + * + * @param dtm The data type manager to search in. + * @param typeName The name of the type to find. + * @return The DataType if found, or null if not found. + */ + private DataType findDataTypeByNameInAllCategories(DataTypeManager dtm, String typeName) { + // Try exact match first + DataType result = searchByNameInAllCategories(dtm, typeName); + if (result != null) { + return result; + } + + // Try lowercase + return searchByNameInAllCategories(dtm, typeName.toLowerCase()); + } + + /** + * Searches for a data type by name in all categories of the DataTypeManager. + * This method checks both exact and case-insensitive matches. + * + * @param dtm The DataTypeManager to search in. + * @param name The name of the data type to search for. + * @return The DataType if found, or null if not found. + */ + private DataType searchByNameInAllCategories(DataTypeManager dtm, String name) { + // Get all data types from the manager + Iterator allTypes = dtm.getAllDataTypes(); + while (allTypes.hasNext()) { + DataType dt = allTypes.next(); + // Check if the name matches exactly (case-sensitive) + if (dt.getName().equals(name)) { + return dt; + } + // For case-insensitive, we want an exact match except for case + if (dt.getName().equalsIgnoreCase(name)) { + return dt; + } + } + return null; + } + + /** + * Find a symbol by name in the local symbol map of the high function. + * + * @param highFunction The high function to search in. + * @param variableName The name of the variable to find. + * @return The HighSymbol if found, or null if not found. + */ + private HighSymbol findSymbolByName(ghidra.program.model.pcode.HighFunction highFunction, String variableName) { + Iterator symbols = highFunction.getLocalSymbolMap().getSymbols(); + while (symbols.hasNext()) { + HighSymbol s = symbols.next(); + if (s.getName().equals(variableName)) { + return s; + } + } + return null; + } + + /** + * Decompile the function to access its high-level representation. + * + * @param func The function to decompile. + * @param program The current program. + * @return The DecompileResults containing the decompiled function. + */ + private DecompileResults decompileFunction(Function func, Program program) { + // Set up decompiler for accessing the decompiled function + DecompInterface decomp = new DecompInterface(); + decomp.openProgram(program); + decomp.setSimplificationStyle("decompile"); // Full decompilation + + // Decompile the function + DecompileResults results = decomp.decompileFunction(func, 60, new ConsoleTaskMonitor()); + + if (!results.decompileCompleted()) { + Msg.error(this, "Could not decompile function: " + results.getErrorMessage()); + return null; + } + + return results; + } + + /** + * Update the variable type in the database using HighFunctionDBUtil. + * + * @param program The current program. + * @param symbol The high symbol representing the variable. + * @param dataType The new data type to set for the variable. + * @param success AtomicBoolean to indicate if the operation was successful. + */ + private void updateVariableType(Program program, HighSymbol symbol, DataType dataType, AtomicBoolean success) { + int tx = program.startTransaction("Set variable type"); + try { + // Use HighFunctionDBUtil to update the variable with the new type + HighFunctionDBUtil.updateDBVariable( + symbol, // The high symbol to modify + symbol.getName(), // Keep original name + dataType, // The new data type + SourceType.USER_DEFINED // Mark as user-defined + ); + + success.set(true); + Msg.info(this, "Successfully set variable type using HighFunctionDBUtil"); + } catch (Exception e) { + Msg.error(this, "Error setting variable type: " + e.getMessage()); + } finally { + program.endTransaction(tx, success.get()); + } + } +} diff --git a/src/main/java/com/lauriewired/util/GhidraUtils.java b/src/main/java/com/lauriewired/util/GhidraUtils.java new file mode 100644 index 0000000..dd0a81e --- /dev/null +++ b/src/main/java/com/lauriewired/util/GhidraUtils.java @@ -0,0 +1,117 @@ +package com.lauriewired.util; + +import ghidra.app.services.ProgramManager; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.data.DataType; +import ghidra.program.model.data.DataTypeManager; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; +import ghidra.util.Msg; + +import javax.swing.*; +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import ghidra.app.services.DataTypeManagerService; +import static ghidra.program.util.GhidraProgramUtilities.getCurrentProgram; +import ghidra.util.data.DataTypeParser; +import ghidra.util.data.DataTypeParser.AllowedDataTypes; + +/** + * Utility class for Ghidra-related operations. + * Provides methods to interact with the current program, resolve data types, + * and set comments at specific addresses. + */ +public final class GhidraUtils { + /** + * Gets the current program from the specified plugin tool. + * + * @param tool the plugin tool + * @return the current program, or null if not available + */ + public static Program getCurrentProgram(PluginTool tool) { + ProgramManager pm = tool.getService(ProgramManager.class); + return pm != null ? pm.getCurrentProgram() : null; + } + + /** + * Resolves a data type by name, handling common types and pointer types + * + * @param tool The plugin tool to use for services + * @param dtm The data type manager + * @param typeName The type name to resolve + * @return The resolved DataType, or null if not found + */ + public static DataType resolveDataType(PluginTool tool, DataTypeManager dtm, String typeName) { + DataTypeManagerService dtms = tool.getService(DataTypeManagerService.class); + DataTypeManager[] managers = dtms.getDataTypeManagers(); + DataType dt = null; + + List managerList = new ArrayList<>(); + for (DataTypeManager manager : managers) { + if (manager != dtm) + managerList.add(manager); + } + managerList.addFirst(dtm); + + DataTypeParser parser = null; + + for (DataTypeManager manager : managerList) { + try { + parser = new DataTypeParser(manager, null, null, AllowedDataTypes.ALL); + dt = parser.parse(typeName); + if (dt != null) { + return dt; // Found a successful parse, return + } + } catch (Exception e) { + // Continue to next manager if this one fails + } + } + + // Fallback to int if we couldn't find it + Msg.warn(GhidraUtils.class, "Unknown type: " + typeName + ", defaulting to int"); + return dtm.getDataType("/int"); + } + + /** + * Sets a comment at the specified address in the current program. + * + * @param tool the plugin tool + * @param addressStr the address as a string + * @param comment the comment to set + * @param commentType the type of comment (e.g., CodeUnit.PLATE_COMMENT) + * @param transactionName the name of the transaction for logging + * @return true if successful, false otherwise + */ + public static boolean setCommentAtAddress(PluginTool tool, + String addressStr, String comment, int commentType, String transactionName) { + Program program = getCurrentProgram(tool); + if (program == null) + return false; + if (addressStr == null || addressStr.isEmpty() || comment == null) + return false; + + AtomicBoolean success = new AtomicBoolean(false); + + try { + SwingUtilities.invokeAndWait(() -> { + int tx = program.startTransaction(transactionName); + try { + Address addr = program.getAddressFactory().getAddress(addressStr); + program.getListing().setComment(addr, commentType, comment); + success.set(true); + } catch (Exception e) { + Msg.error(GhidraUtils.class, "Error setting " + transactionName.toLowerCase(), e); + } finally { + program.endTransaction(tx, success.get()); + } + }); + } catch (InterruptedException | InvocationTargetException e) { + Msg.error(GhidraUtils.class, + "Failed to execute " + transactionName.toLowerCase() + " on Swing thread", e); + } + + return success.get(); + } +} diff --git a/src/main/java/com/lauriewired/util/ParseUtils.java b/src/main/java/com/lauriewired/util/ParseUtils.java new file mode 100644 index 0000000..aa10ae8 --- /dev/null +++ b/src/main/java/com/lauriewired/util/ParseUtils.java @@ -0,0 +1,195 @@ +package com.lauriewired.util; + +import com.sun.net.httpserver.HttpExchange; +import ghidra.program.model.address.Address; +import ghidra.util.Msg; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Utility methods for parsing HTTP requests and responses. + * + * This class provides methods to parse query parameters, post body parameters, + * paginate lists, parse integers with defaults, escape non-ASCII characters, + * and send HTTP responses. + */ +public final class ParseUtils { + /** + * Parse query parameters from the request URI. + * + * @param exchange The HttpExchange object containing the request. + * @return A map of query parameters where the key is the parameter name + * and the value is the parameter value. + * For example, for a query string "offset=10&limit=100", + * the map will contain {"offset": "10", "limit": "100"} + */ + public static Map parseQueryParams(HttpExchange exchange) { + Map result = new HashMap<>(); + String query = exchange.getRequestURI().getQuery(); // e.g. offset=10&limit=100 + if (query != null) { + String[] pairs = query.split("&"); + for (String p : pairs) { + String[] kv = p.split("="); + if (kv.length == 2) { + // URL decode parameter values + try { + String key = URLDecoder.decode(kv[0], StandardCharsets.UTF_8); + String value = URLDecoder.decode(kv[1], StandardCharsets.UTF_8); + result.put(key, value); + } catch (Exception e) { + Msg.error(ParseUtils.class, "Error decoding URL parameter", e); + } + } + } + } + return result; + } + + /** + * Parse POST parameters from the request body. + * + * @param exchange The HttpExchange object containing the request. + * @return A map of POST parameters where the key is the parameter name + * and the value is the parameter value. + * For example, for a body "offset=10&limit=100", + * the map will contain {"offset": "10", "limit": "100"} + */ + public static Map parsePostParams(HttpExchange exchange) throws IOException { + byte[] body = exchange.getRequestBody().readAllBytes(); + String bodyStr = new String(body, StandardCharsets.UTF_8); + Map params = new HashMap<>(); + for (String pair : bodyStr.split("&")) { + String[] kv = pair.split("="); + if (kv.length == 2) { + // URL decode parameter values + try { + String key = URLDecoder.decode(kv[0], StandardCharsets.UTF_8); + String value = URLDecoder.decode(kv[1], StandardCharsets.UTF_8); + params.put(key, value); + } catch (Exception e) { + Msg.error(ParseUtils.class, "Error decoding URL parameter", e); + } + } + } + return params; + } + + /** + * Paginate a list of items based on offset and limit. + * + * @param items The list of items to paginate. + * @param offset The starting index for pagination. + * @param limit The maximum number of items to return. + * @return A string containing the paginated items, each on a new line. + * If the offset is beyond the list size, returns an empty string. + */ + public static String paginateList(List items, int offset, int limit) { + int start = Math.max(0, offset); + int end = Math.min(items.size(), offset + limit); + + if (start >= items.size()) { + return ""; // no items in range + } + List sub = items.subList(start, end); + return String.join("\n", sub); + } + + /** + * Parse an integer from a string, returning a default value if parsing fails. + * + * @param val The string to parse. + * @param defaultValue The default value to return if parsing fails. + * @return The parsed integer or the default value if parsing fails. + */ + public static int parseIntOrDefault(String val, int defaultValue) { + if (val == null) + return defaultValue; + try { + return Integer.parseInt(val); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + /** + * Escape non-ASCII characters in a string. + * + * @param input The input string to escape. + * @return A string where non-ASCII characters are replaced with their + * hexadecimal representation, e.g. "\xFF" for 255. + */ + public static String escapeNonAscii(String input) { + if (input == null) + return ""; + StringBuilder sb = new StringBuilder(); + for (char c : input.toCharArray()) { + if (c >= 32 && c < 127) { + sb.append(c); + } else { + sb.append("\\x"); + sb.append(Integer.toHexString(c & 0xFF)); + } + } + return sb.toString(); + } + + /** + * Send a plain text response to the HTTP exchange. + * + * @param exchange The HttpExchange object to send the response to. + * @param response The response string to send. + * @throws IOException If an I/O error occurs while sending the response. + */ + public static void sendResponse(HttpExchange exchange, String response) throws IOException { + byte[] bytes = response.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8"); + exchange.sendResponseHeaders(200, bytes.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(bytes); + } + } + + /** + * Generate a hexdump of a byte array starting from a given base address. + * + * @param base The base address to start the hexdump from. + * @param buf The byte array to generate the hexdump for. + * @param len The number of bytes to include in the hexdump. + * @return A string representation of the hexdump. + */ + public static String hexdump(Address base, byte[] buf, int len) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len; i += 16) { + sb.append(String.format("%s ", base.add(i))); + for (int j = 0; j < 16 && (i + j) < len; j++) { + sb.append(String.format("%02X ", buf[i + j])); + } + sb.append('\n'); + } + return sb.toString(); + } + + /** + * Decode a hexadecimal string into a byte array. + * + * @param hex The hexadecimal string to decode. + * @return A byte array representing the decoded hexadecimal string. + * @throws IllegalArgumentException If the input string is not a valid hex string. + */ + public static byte[] decodeHex(String hex) { + hex = hex.replaceAll("\\s+", ""); + if (hex.length() % 2 != 0) + throw new IllegalArgumentException(); + byte[] out = new byte[hex.length() / 2]; + for (int i = 0; i < out.length; i++) { + out[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16); + } + return out; + } +} diff --git a/src/main/java/com/lauriewired/util/StructUtils.java b/src/main/java/com/lauriewired/util/StructUtils.java new file mode 100644 index 0000000..cda5abe --- /dev/null +++ b/src/main/java/com/lauriewired/util/StructUtils.java @@ -0,0 +1,34 @@ +package com.lauriewired.util; + +/** + * Utility class for handling structures and their members. + * This class provides a representation of a structure member with its name, + * type, comment, and offset. + */ +public final class StructUtils { + /** + * Represents a member of a structure. + */ + public static class StructMember { + /** + * The name of the member. + */ + public String name; + + /** + * The type of the member. + */ + public String type; + + /** + * The comment for the member. + */ + public String comment; + + /** + * The offset of the member in the structure. + * Initialized to -1 to indicate that it has not been set. + */ + public double offset = -1; // Use double to handle GSON parsing number as double + } +} diff --git a/src/test/java/com/lauriewired/AppTest.java b/src/test/java/com/lauriewired/AppTest.java index 77b1a97..a39d69e 100644 --- a/src/test/java/com/lauriewired/AppTest.java +++ b/src/test/java/com/lauriewired/AppTest.java @@ -7,32 +7,28 @@ /** * Unit test for simple App. */ -public class AppTest - extends TestCase -{ - /** - * Create the test case - * - * @param testName name of the test case - */ - public AppTest( String testName ) - { - super( testName ); - } +public class AppTest + extends TestCase { + /** + * Create the test case + * + * @param testName name of the test case + */ + public AppTest(String testName) { + super(testName); + } - /** - * @return the suite of tests being tested - */ - public static Test suite() - { - return new TestSuite( AppTest.class ); - } + /** + * @return the suite of tests being tested + */ + public static Test suite() { + return new TestSuite(AppTest.class); + } - /** - * Rigourous Test :-) - */ - public void testApp() - { - assertTrue( true ); - } + /** + * Rigourous Test :-) + */ + public void testApp() { + assertTrue(true); + } }