From 2667c3612100193bc688d8669e9be29577b4a8e3 Mon Sep 17 00:00:00 2001 From: Abdul Samad Date: Thu, 25 Jun 2026 23:43:51 +0500 Subject: [PATCH 1/2] Extend ExportFunctionInfoScript with metrics and call graph exports Consolidate JSON metrics, CSV metrics, and DOT call graph export into the existing script with format selection, addressing PR #8574 review feedback. --- .../ExportFunctionInfoScript.java | 439 ++++++++++++++++-- 1 file changed, 413 insertions(+), 26 deletions(-) diff --git a/Ghidra/Features/Base/ghidra_scripts/ExportFunctionInfoScript.java b/Ghidra/Features/Base/ghidra_scripts/ExportFunctionInfoScript.java index 528786f7e9..aa6718162f 100644 --- a/Ghidra/Features/Base/ghidra_scripts/ExportFunctionInfoScript.java +++ b/Ghidra/Features/Base/ghidra_scripts/ExportFunctionInfoScript.java @@ -4,60 +4,447 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -// List function names and entry point addresses to a file in JSON format +// Export function information in various formats (JSON, CSV, DOT) //@category Functions +import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import com.google.gson.*; import com.google.gson.stream.JsonWriter; import ghidra.app.script.GhidraScript; import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.block.CodeBlockIterator; +import ghidra.program.model.block.SimpleBlockModel; +import ghidra.program.model.data.DataType; import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.Namespace; +import ghidra.program.util.CyclomaticComplexity; +import ghidra.util.Msg; +import ghidra.util.exception.CancelledException; +import util.CollectionUtils; public class ExportFunctionInfoScript extends GhidraScript { private static final String NAME = "name"; private static final String ENTRY = "entry"; - @Override - public void run() throws Exception { + private enum ExportFormat { + JSON_SIMPLE("JSON (Simple - name and entry only)"), + JSON_METRICS("JSON (Detailed metrics)"), + CSV_METRICS("CSV (Detailed metrics)"), + DOT_CALLGRAPH("DOT (Function call graph)"); - Gson gson = new GsonBuilder().setPrettyPrinting().create(); + private final String displayName; - File outputFile = askFile("Please Select Output File", "Choose"); - JsonWriter jsonWriter = new JsonWriter(new FileWriter(outputFile)); - jsonWriter.beginArray(); - - Listing listing = currentProgram.getListing(); - FunctionIterator iter = listing.getFunctions(true); - while (iter.hasNext() && !monitor.isCancelled()) { - Function f = iter.next(); - - String name = f.getName(); - Address entry = f.getEntryPoint(); - - JsonObject json = new JsonObject(); - json.addProperty(NAME, name); - json.addProperty(ENTRY, entry.toString()); - - gson.toJson(json, jsonWriter); + ExportFormat(String displayName) { + this.displayName = displayName; } - jsonWriter.endArray(); - jsonWriter.close(); + @Override + public String toString() { + return displayName; + } + } - println("Wrote functions to " + outputFile); + @Override + public void run() throws Exception { + if (currentProgram == null) { + printerr("No current program."); + return; + } + + List formats = Arrays.asList(ExportFormat.values()); + ExportFormat selectedFormat = askChoice("Choose Export Format", + "Select the format for exporting function information:", formats, + ExportFormat.JSON_SIMPLE); + + File outputFile = askFile("Please Select Output File", "Choose"); + if (outputFile == null) { + printerr("No output file selected."); + return; + } + + AddressSetView scope = currentSelection != null ? currentSelection : currentHighlight; + + switch (selectedFormat) { + case JSON_SIMPLE: + exportSimpleJson(outputFile, scope); + break; + case JSON_METRICS: + exportMetricsJson(outputFile, scope); + break; + case CSV_METRICS: + exportMetricsCsv(outputFile, scope); + break; + case DOT_CALLGRAPH: + exportCallGraphDot(outputFile, scope); + break; + } + + println("Wrote function information to: " + outputFile.getAbsolutePath()); + } + + private void exportSimpleJson(File outputFile, AddressSetView scope) + throws IOException, CancelledException { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + try (JsonWriter jsonWriter = new JsonWriter(new FileWriter(outputFile))) { + jsonWriter.beginArray(); + + Listing listing = currentProgram.getListing(); + FunctionIterator iter = listing.getFunctions(true); + while (iter.hasNext()) { + monitor.checkCancelled(); + Function f = iter.next(); + + if (scope != null && !scope.intersects(f.getBody())) { + continue; + } + + JsonObject json = new JsonObject(); + json.addProperty(NAME, f.getName()); + json.addProperty(ENTRY, f.getEntryPoint().toString()); + + gson.toJson(json, jsonWriter); + } + + jsonWriter.endArray(); + } + } + + private void exportMetricsJson(File outputFile, AddressSetView scope) + throws IOException, CancelledException { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + FunctionManager fm = currentProgram.getFunctionManager(); + CyclomaticComplexity complexityCalc = new CyclomaticComplexity(); + + JsonObject root = new JsonObject(); + + JsonObject programInfo = new JsonObject(); + programInfo.addProperty("name", currentProgram.getName()); + programInfo.addProperty("language", currentProgram.getLanguageID().getIdAsString()); + root.add("program", programInfo); + + JsonArray functions = new JsonArray(); + FunctionIterator it = fm.getFunctions(true); + monitor.initialize(fm.getFunctionCount()); + int exported = 0; + + while (it.hasNext()) { + monitor.checkCancelled(); + Function f = it.next(); + AddressSetView body = f.getBody(); + if (scope != null && !scope.intersects(body)) { + continue; + } + + monitor.setMessage("Exporting metrics: " + f.getName()); + monitor.incrementProgress(1); + + functions.add(buildFunctionMetricsJson(f, body, complexityCalc)); + exported++; + } + + root.add("functions", functions); + + JsonObject summary = new JsonObject(); + summary.addProperty("exported_functions", exported); + summary.addProperty("total_functions", fm.getFunctionCount()); + summary.addProperty("selection_applied", scope != null); + root.add("summary", summary); + + try (FileWriter writer = new FileWriter(outputFile)) { + gson.toJson(root, writer); + } + } + + private JsonObject buildFunctionMetricsJson(Function f, AddressSetView body, + CyclomaticComplexity complexityCalc) throws CancelledException { + Listing listing = currentProgram.getListing(); + + int instCount = (int) CollectionUtils.asStream(listing.getInstructions(body, true)) + .count(); + int callers = sizeSafe(f.getCallingFunctions(monitor)); + int callees = sizeSafe(f.getCalledFunctions(monitor)); + + int cplx = 0; + try { + cplx = complexityCalc.calculateCyclomaticComplexity(f, monitor); + } + catch (Exception e) { + Msg.warn(this, "Failed to compute complexity for " + f.getName(), e); + } + + int localsCount = 0; + if (!f.isExternal()) { + localsCount = f.getLocalVariables().length; + } + + String thunkTarget = null; + if (f.isThunk()) { + Function tf = f.getThunkedFunction(true); + if (tf != null) { + thunkTarget = tf.getEntryPoint().toString(); + } + } + + Namespace ns = f.getParentNamespace(); + DataType retType = f.getReturnType(); + + JsonObject json = new JsonObject(); + json.addProperty("name", f.getName()); + json.addProperty("entry", f.getEntryPoint().toString()); + json.addProperty("size_bytes", body.getNumAddresses()); + json.addProperty("address_ranges", body.getNumAddressRanges()); + json.addProperty("instruction_count", instCount); + json.addProperty("cyclomatic_complexity", cplx); + json.addProperty("parameters", f.getParameterCount()); + json.addProperty("locals", localsCount); + json.addProperty("basic_blocks", getBasicBlockCount(body)); + json.addProperty("external", f.isExternal()); + json.addProperty("thunk", f.isThunk()); + if (thunkTarget == null) { + json.add("thunk_target", JsonNull.INSTANCE); + } + else { + json.addProperty("thunk_target", thunkTarget); + } + json.addProperty("variadic", f.hasVarArgs()); + json.addProperty("inline", f.isInline()); + json.addProperty("no_return", f.hasNoReturn()); + json.addProperty("custom_storage", f.hasCustomVariableStorage()); + json.addProperty("stack_purge_size", f.getStackPurgeSize()); + json.addProperty("calling_convention", safeString(f.getCallingConventionName())); + json.addProperty("namespace", ns != null ? ns.getName(true) : ""); + json.addProperty("return_type", + retType != null ? retType.getDisplayName() : "void"); + json.addProperty("prototype", f.getPrototypeString(true, false)); + json.addProperty("prototype_with_cc", f.getPrototypeString(true, true)); + json.addProperty("callers", callers); + json.addProperty("callees", callees); + return json; + } + + private void exportMetricsCsv(File outputFile, AddressSetView scope) + throws IOException, CancelledException { + FunctionManager fm = currentProgram.getFunctionManager(); + Listing listing = currentProgram.getListing(); + CyclomaticComplexity complexityCalc = new CyclomaticComplexity(); + + try (BufferedWriter w = new BufferedWriter(new FileWriter(outputFile))) { + w.write(String.join(",", "name", "entry", "address_ranges", "instruction_count", + "cyclomatic_complexity", "parameters", "locals", "basic_blocks", "external", + "thunk", "thunk_target", "variadic", "inline", "no_return", "custom_storage", + "stack_purge_size", "calling_convention", "namespace", "return_type", + "prototype", "prototype_with_cc", "callers", "callees")); + w.write("\n"); + + FunctionIterator it = fm.getFunctions(true); + monitor.initialize(fm.getFunctionCount()); + + while (it.hasNext()) { + monitor.checkCancelled(); + Function f = it.next(); + AddressSetView body = f.getBody(); + if (scope != null && !scope.intersects(body)) { + continue; + } + + monitor.setMessage("Exporting metrics: " + f.getName()); + monitor.incrementProgress(1); + + String entry = f.getEntryPoint().toString(); + int instCount = (int) CollectionUtils.asStream(listing.getInstructions(body, true)) + .count(); + + int callers = sizeSafe(f.getCallingFunctions(monitor)); + int callees = sizeSafe(f.getCalledFunctions(monitor)); + + int cplx = 0; + try { + cplx = complexityCalc.calculateCyclomaticComplexity(f, monitor); + } + catch (Exception e) { + Msg.warn(this, "Failed to compute complexity for " + f.getName(), e); + } + + int localsCount = 0; + if (!f.isExternal()) { + localsCount = f.getLocalVariables().length; + } + + String thunkTarget = null; + if (f.isThunk()) { + Function tf = f.getThunkedFunction(true); + if (tf != null) { + thunkTarget = tf.getEntryPoint().toString(); + } + } + + Namespace ns = f.getParentNamespace(); + String namespace = ns != null ? ns.getName(true) : ""; + DataType retType = f.getReturnType(); + String returnType = retType != null ? retType.getDisplayName() : "void"; + String callConv = safeString(f.getCallingConventionName()); + String proto = f.getPrototypeString(true, false); + String protoWithCc = f.getPrototypeString(true, true); + + w.write(String.join(",", csv(f.getName()), csv(entry), + Integer.toString(body.getNumAddressRanges()), Integer.toString(instCount), + Integer.toString(cplx), Integer.toString(f.getParameterCount()), + Integer.toString(localsCount), Integer.toString(getBasicBlockCount(body)), + Boolean.toString(f.isExternal()), Boolean.toString(f.isThunk()), + csvOrEmpty(thunkTarget), Boolean.toString(f.hasVarArgs()), + Boolean.toString(f.isInline()), Boolean.toString(f.hasNoReturn()), + Boolean.toString(f.hasCustomVariableStorage()), + Integer.toString(f.getStackPurgeSize()), csv(callConv), csv(namespace), + csv(returnType), csv(proto), csv(protoWithCc), + Integer.toString(callers), Integer.toString(callees))); + w.write("\n"); + } + } + } + + private void exportCallGraphDot(File outputFile, AddressSetView scope) + throws IOException, CancelledException { + FunctionManager fm = currentProgram.getFunctionManager(); + FunctionIterator it = fm.getFunctions(true); + + Map idByEntry = new HashMap<>(); + Set edges = new HashSet<>(); + + monitor.initialize(fm.getFunctionCount()); + + while (it.hasNext()) { + monitor.checkCancelled(); + Function f = it.next(); + if (scope != null && !scope.intersects(f.getBody())) { + continue; + } + monitor.setMessage("Indexing function: " + f.getName()); + monitor.incrementProgress(1); + + String fEntry = f.getEntryPoint().toString(); + assignFunctionId(idByEntry, fEntry); + + for (Function callee : f.getCalledFunctions(monitor)) { + monitor.checkCancelled(); + if (scope != null && !scope.intersects(callee.getBody())) { + continue; + } + String cEntry = callee.getEntryPoint().toString(); + assignFunctionId(idByEntry, cEntry); + edges.add(fEntry + "->" + cEntry); + } + } + + try (BufferedWriter w = new BufferedWriter(new FileWriter(outputFile))) { + w.write("digraph \"" + escapeDot(currentProgram.getName()) + "\" {\n"); + w.write(" node [shape=box, fontsize=10];\n"); + + for (Map.Entry e : idByEntry.entrySet()) { + String entry = e.getKey(); + int id = e.getValue(); + Function f = + fm.getFunctionAt(currentProgram.getAddressFactory().getAddress(entry)); + String name = f != null ? f.getName() : entry; + String label = name + "\n" + entry; + w.write(" n" + id + " [label=\"" + escapeDot(label) + "\"];\n"); + } + + for (String edge : edges) { + String[] parts = edge.split("->", 2); + Integer sId = idByEntry.get(parts[0]); + Integer tId = idByEntry.get(parts[1]); + if (sId != null && tId != null) { + w.write(" n" + sId + " -> n" + tId + ";\n"); + } + } + + w.write("}\n"); + } + } + + private int getBasicBlockCount(AddressSetView body) throws CancelledException { + SimpleBlockModel model = new SimpleBlockModel(currentProgram); + CodeBlockIterator blocks = model.getCodeBlocksContaining(body, monitor); + int basicBlocks = 0; + while (blocks.hasNext()) { + monitor.checkCancelled(); + blocks.next(); + basicBlocks++; + } + return basicBlocks; + } + + private static void assignFunctionId(Map idByEntry, String entry) { + if (!idByEntry.containsKey(entry)) { + idByEntry.put(entry, idByEntry.size() + 1); + } + } + + private static int sizeSafe(Set s) { + return s == null ? 0 : s.size(); + } + + private static String safeString(String s) { + return s == null ? "" : s; + } + + private static String escapeDot(String s) { + if (s == null) { + return ""; + } + StringBuilder b = new StringBuilder(s.length() + 16); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '"': + b.append("\\\""); + break; + case '\\': + b.append("\\\\"); + break; + case '\n': + b.append("\\n"); + break; + default: + b.append(c); + break; + } + } + return b.toString(); + } + + private static String csv(String s) { + if (s == null) { + return "\"\""; + } + String v = s.replace("\"", "\"\""); + return "\"" + v + "\""; + } + + private static String csvOrEmpty(String s) { + return s == null ? "" : csv(s); } } From d9a2d109fb5363094a8bdb28fae4ef88105efee1 Mon Sep 17 00:00:00 2001 From: Ryan Kurtz Date: Wed, 1 Jul 2026 05:24:45 -0400 Subject: [PATCH 2/2] GP-7031: Updating test and fixing warning --- .../ExportFunctionInfoScript.java | 17 ++++------------- .../script/ExportFunctionInfoScriptTest.java | 10 ++++++++-- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Ghidra/Features/Base/ghidra_scripts/ExportFunctionInfoScript.java b/Ghidra/Features/Base/ghidra_scripts/ExportFunctionInfoScript.java index aa6718162f..6076fd96dd 100644 --- a/Ghidra/Features/Base/ghidra_scripts/ExportFunctionInfoScript.java +++ b/Ghidra/Features/Base/ghidra_scripts/ExportFunctionInfoScript.java @@ -16,22 +16,13 @@ // Export function information in various formats (JSON, CSV, DOT) //@category Functions -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.io.*; +import java.util.*; import com.google.gson.*; import com.google.gson.stream.JsonWriter; import ghidra.app.script.GhidraScript; -import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressSetView; import ghidra.program.model.block.CodeBlockIterator; import ghidra.program.model.block.SimpleBlockModel; @@ -173,8 +164,8 @@ public class ExportFunctionInfoScript extends GhidraScript { summary.addProperty("selection_applied", scope != null); root.add("summary", summary); - try (FileWriter writer = new FileWriter(outputFile)) { - gson.toJson(root, writer); + try (FileWriter w = new FileWriter(outputFile)) { + gson.toJson(root, w); } } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/ExportFunctionInfoScriptTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/ExportFunctionInfoScriptTest.java index 4f70cd7450..04379f40e7 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/ExportFunctionInfoScriptTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/ExportFunctionInfoScriptTest.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,6 +15,7 @@ */ package ghidra.app.plugin.core.script; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; @@ -22,6 +23,8 @@ import java.io.*; import java.util.ArrayList; import java.util.List; +import javax.swing.JDialog; + import org.junit.Before; import org.junit.Test; @@ -77,6 +80,9 @@ public class ExportFunctionInfoScriptTest extends AbstractGhidraHeadedIntegratio ScriptTaskListener listener = env.runScript(script); + JDialog dialog = waitForJDialog("Choose Export Format"); + pressButtonByText(dialog, "OK"); + chooseFile(outputFile); waitForScriptCompletion(listener, 20000);