Skip to main content
Glama
DecompiledTextSearchService.java13.1 kB
package com.ghidramcp.services; import ghidra.app.decompiler.DecompInterface; import ghidra.app.decompiler.DecompileOptions; import ghidra.app.decompiler.DecompileResults; import ghidra.program.model.listing.*; import ghidra.util.Msg; import ghidra.util.task.ConsoleTaskMonitor; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; /** * Service for searching through decompiled function code */ public class DecompiledTextSearchService { private final FunctionNavigator navigator; private final int decompileTimeout; public DecompiledTextSearchService(FunctionNavigator navigator, int decompileTimeout) { this.navigator = navigator; this.decompileTimeout = decompileTimeout; } /** * Search for a pattern in decompiled functions * @param patternStr Search pattern (regex or literal string) * @param isRegex Whether pattern is regex (true) or literal string (false) * @param caseSensitive Whether search is case-sensitive * @param multiline Whether pattern can match across multiple lines * @param functionNames Optional list of specific function names to search (null for all) * @param maxResults Maximum number of results to return (0 for unlimited) * @param offset Pagination offset * @param limit Pagination limit * @return JSON string with search results */ public String searchDecompiledText(String patternStr, boolean isRegex, boolean caseSensitive, boolean multiline, List<String> functionNames, int maxResults, int offset, int limit) { Program program = navigator.getCurrentProgram(); if (program == null) { return "{\"error\": \"No program loaded\"}"; } if (patternStr == null || patternStr.isEmpty()) { return "{\"error\": \"Pattern is required\"}"; } // Compile the pattern Pattern pattern; try { pattern = compilePattern(patternStr, isRegex, caseSensitive, multiline); } catch (PatternSyntaxException e) { return String.format("{\"error\": \"Invalid regex pattern: %s\"}", escapeJson(e.getMessage())); } // Get functions to search List<Function> functionsToSearch = getFunctionsToSearch(program, functionNames); if (functionsToSearch.isEmpty()) { return "{\"matches\": [], \"count\": 0, \"message\": \"No functions to search\"}"; } // Search through functions List<SearchMatch> allMatches = new ArrayList<>(); DecompInterface decomp = null; try { decomp = setupDecompiler(program); for (Function func : functionsToSearch) { // Stop if we've reached maxResults (if specified) if (maxResults > 0 && allMatches.size() >= maxResults) { break; } // Skip thunk and external functions if (func.isThunk() || func.isExternal()) { continue; } // Decompile the function DecompileResults results = decomp.decompileFunction(func, decompileTimeout, new ConsoleTaskMonitor()); decomp.flushCache(); if (results == null || !results.decompileCompleted()) { continue; } String decompiled = results.getDecompiledFunction().getC(); if (decompiled == null || decompiled.isEmpty()) { continue; } // Search for pattern in decompiled code List<SearchMatch> functionMatches = searchInText(func, decompiled, pattern, multiline); allMatches.addAll(functionMatches); } } catch (Exception e) { Msg.error(this, "Error searching decompiled text", e); return String.format("{\"error\": \"Search failed: %s\"}", escapeJson(e.getMessage())); } finally { if (decomp != null) { decomp.dispose(); } } // Apply pagination List<SearchMatch> paginatedMatches = applyPagination(allMatches, offset, limit); // Format results as JSON return formatResultsAsJson(paginatedMatches, allMatches.size(), offset, limit); } /** * Compile a pattern with the specified flags */ private Pattern compilePattern(String patternStr, boolean isRegex, boolean caseSensitive, boolean multiline) { String actualPattern = isRegex ? patternStr : Pattern.quote(patternStr); int flags = 0; if (!caseSensitive) { flags |= Pattern.CASE_INSENSITIVE; } if (multiline) { flags |= Pattern.DOTALL; } return Pattern.compile(actualPattern, flags); } /** * Get functions to search based on filter */ private List<Function> getFunctionsToSearch(Program program, List<String> functionNames) { List<Function> functions = new ArrayList<>(); FunctionManager funcManager = program.getFunctionManager(); if (functionNames == null || functionNames.isEmpty()) { // Search all functions FunctionIterator iter = funcManager.getFunctions(true); while (iter.hasNext()) { functions.add(iter.next()); } } else { // Search specific functions for (String name : functionNames) { FunctionIterator iter = funcManager.getFunctions(true); while (iter.hasNext()) { Function func = iter.next(); if (func.getName().equals(name)) { functions.add(func); break; } } } } return functions; } /** * Set up decompiler with appropriate options */ private DecompInterface setupDecompiler(Program program) { DecompInterface decomp = new DecompInterface(); DecompileOptions options = new DecompileOptions(); decomp.setOptions(options); decomp.setSimplificationStyle("decompile"); decomp.openProgram(program); return decomp; } /** * Search for pattern in decompiled text */ private List<SearchMatch> searchInText(Function func, String text, Pattern pattern, boolean multiline) { List<SearchMatch> matches = new ArrayList<>(); if (multiline) { // Multi-line search: search entire text at once Matcher matcher = pattern.matcher(text); while (matcher.find()) { int matchStart = matcher.start(); int matchEnd = matcher.end(); int lineNumber = getLineNumber(text, matchStart); String context = extractContext(text, matchStart, matchEnd); String matchedText = matcher.group(); matches.add(new SearchMatch( func.getName(), func.getEntryPoint().toString(), lineNumber, matchedText, context, true )); } } else { // Single-line search: search line by line String[] lines = text.split("\n"); for (int i = 0; i < lines.length; i++) { String line = lines[i]; Matcher matcher = pattern.matcher(line); while (matcher.find()) { String matchedText = matcher.group(); String context = highlightMatch(line, matcher.start(), matcher.end()); matches.add(new SearchMatch( func.getName(), func.getEntryPoint().toString(), i + 1, // Line numbers are 1-based matchedText, context, false )); } } } return matches; } /** * Get line number for a character position in text */ private int getLineNumber(String text, int position) { int lineNumber = 1; for (int i = 0; i < position && i < text.length(); i++) { if (text.charAt(i) == '\n') { lineNumber++; } } return lineNumber; } /** * Extract context around a match (up to 200 characters before and after) */ private String extractContext(String text, int matchStart, int matchEnd) { int contextStart = Math.max(0, matchStart - 100); int contextEnd = Math.min(text.length(), matchEnd + 100); // Adjust to line boundaries if possible while (contextStart > 0 && text.charAt(contextStart) != '\n') { contextStart--; } while (contextEnd < text.length() && text.charAt(contextEnd) != '\n') { contextEnd++; } String context = text.substring(contextStart, contextEnd).trim(); // Add ellipsis if truncated if (contextStart > 0) context = "..." + context; if (contextEnd < text.length()) context = context + "..."; return context; } /** * Highlight match in line with markers */ private String highlightMatch(String line, int start, int end) { StringBuilder result = new StringBuilder(); result.append(line.substring(0, start)); result.append("[["); result.append(line.substring(start, end)); result.append("]]"); if (end < line.length()) { result.append(line.substring(end)); } return result.toString(); } /** * Apply pagination to results */ private List<SearchMatch> applyPagination(List<SearchMatch> matches, int offset, int limit) { if (offset >= matches.size()) { return new ArrayList<>(); } int end = Math.min(matches.size(), offset + limit); return new ArrayList<>(matches.subList(offset, end)); } /** * Format results as JSON */ private String formatResultsAsJson(List<SearchMatch> matches, int totalCount, int offset, int limit) { StringBuilder json = new StringBuilder(); json.append("{"); json.append("\"matches\": ["); for (int i = 0; i < matches.size(); i++) { if (i > 0) json.append(", "); json.append(matches.get(i).toJson()); } json.append("], "); json.append("\"count\": ").append(matches.size()).append(", "); json.append("\"total_count\": ").append(totalCount).append(", "); json.append("\"offset\": ").append(offset).append(", "); json.append("\"limit\": ").append(limit); json.append("}"); return json.toString(); } /** * Escape JSON strings */ private String escapeJson(String str) { if (str == null) return ""; return str.replace("\\", "\\\\") .replace("\"", "\\\"") .replace("\n", "\\n") .replace("\r", "\\r") .replace("\t", "\\t"); } /** * Inner class to represent a search match */ private static class SearchMatch { private final String functionName; private final String functionAddress; private final int lineNumber; private final String matchedText; private final String context; private final boolean isMultiline; public SearchMatch(String functionName, String functionAddress, int lineNumber, String matchedText, String context, boolean isMultiline) { this.functionName = functionName; this.functionAddress = functionAddress; this.lineNumber = lineNumber; this.matchedText = matchedText; this.context = context; this.isMultiline = isMultiline; } public String toJson() { return String.format( "{\"function_name\": \"%s\", \"function_address\": \"%s\", " + "\"line_number\": %d, \"matched_text\": \"%s\", " + "\"context\": \"%s\", \"is_multiline\": %s}", escapeJson(functionName), escapeJson(functionAddress), lineNumber, escapeJson(matchedText), escapeJson(context), isMultiline ); } private static String escapeJson(String str) { if (str == null) return ""; return str.replace("\\", "\\\\") .replace("\"", "\\\"") .replace("\n", "\\n") .replace("\r", "\\r") .replace("\t", "\\t"); } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/HK47196/GhidraMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server