Skip to content

Commit c440158

Browse files
authored
Merge pull request #52 from tmr232/ghidra
Add utils for rendering Ghidra graphs
2 parents 4dab470 + a4b26fc commit c440158

File tree

4 files changed

+186
-26
lines changed

4 files changed

+186
-26
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
1010

1111
- The JetBrains plugin can now change settings: flat switch, simplification, highlighting, and color scheme
1212
- `/render` page to render code directly from GitHub, given a URL with a line number.
13+
- `render-graph.ts` script to render a graph from a JSON file exported from code
14+
- `/render` page can now render a graph provided to it directly.
15+
16+
### Fixed
17+
18+
- `detectBacklinks` no longer has infinite loops on specific cases, and is faster.
1319

1420
## [0.0.12] - 2024-12-18
1521

scripts/render-graph.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { parseArgs } from "node:util";
2+
import { Graphviz } from "@hpcc-js/wasm-graphviz";
3+
import { MultiDirectedGraph } from "graphology";
4+
import type {
5+
CFG,
6+
GraphEdge,
7+
GraphNode,
8+
} from "../src/control-flow/cfg-defs.ts";
9+
import { graphToDot } from "../src/control-flow/render.ts";
10+
11+
async function main() {
12+
const {
13+
positionals: [_runtime, _this, gist_url],
14+
} = parseArgs({
15+
args: Bun.argv,
16+
strict: true,
17+
allowPositionals: true,
18+
});
19+
20+
if (!gist_url) {
21+
throw new Error("Missing URL");
22+
}
23+
24+
const data = await (async () => {
25+
if (gist_url.startsWith("http")) {
26+
const response = await fetch(gist_url);
27+
return response.json();
28+
}
29+
return Bun.file(gist_url).json();
30+
})();
31+
32+
const graph = new MultiDirectedGraph<GraphNode, GraphEdge>();
33+
graph.import(data);
34+
35+
const entry = graph.findNode(
36+
(node, _attributes) => graph.inDegree(node) === 0,
37+
);
38+
if (!entry) {
39+
throw new Error("No entry found");
40+
}
41+
const cfg: CFG = { graph, entry, offsetToNode: [] };
42+
const dot = graphToDot(cfg);
43+
const graphviz = await Graphviz.load();
44+
const svg = graphviz.dot(dot);
45+
console.log(svg);
46+
// console.log(dot);
47+
}
48+
49+
if (require.main === module) {
50+
await main();
51+
}

src/control-flow/graph-ops.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,16 +131,37 @@ export function detectBacklinks(
131131
entry: string,
132132
): { from: string; to: string }[] {
133133
const backlinks: { from: string; to: string }[] = [];
134-
const stack: { node: string; path: string[] }[] = [{ node: entry, path: [] }];
134+
// Using Set() for O(1) path lookups
135+
const stack: { node: string; path: Set<string> }[] = [
136+
{ node: entry, path: new Set<string>() },
137+
];
138+
139+
const alreadyFound = (backlink: { from: string; to: string }): boolean => {
140+
return backlinks.some(
141+
(item) => item.from === backlink.from && item.to === backlink.to,
142+
);
143+
};
144+
145+
// If we ever visit a node that lead to a cycle, we will find the cycle.
146+
// No need to revisit nodes from different paths.
147+
const visited = new Set<string>();
148+
135149
let current = stack.pop();
136150
for (; current !== undefined; current = stack.pop()) {
137151
const { node, path } = current;
152+
if (visited.has(node)) continue;
153+
visited.add(node);
138154
for (const child of graph.outNeighbors(node)) {
139-
if (path.includes(child)) {
140-
backlinks.push({ from: node, to: child });
155+
// Self-loops must be explicitly checked because of the sequence of stack pushes
156+
if (path.has(child) || child === node) {
157+
// Only store backlinks once
158+
const backlink = { from: node, to: child };
159+
if (!alreadyFound(backlink)) {
160+
backlinks.push(backlink);
161+
}
141162
continue;
142163
}
143-
stack.push({ node: child, path: [...path, node] });
164+
stack.push({ node: child, path: new Set(path).add(node) });
144165
}
145166
}
146167

src/render/src/App.svelte

Lines changed: 104 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,26 @@
77
import type Parser from "web-tree-sitter";
88
import { type SyntaxNode } from "web-tree-sitter";
99
import { type Language, newCFGBuilder } from "../../control-flow/cfg";
10-
import { type CFG, mergeNodeAttrs } from "../../control-flow/cfg-defs";
10+
import {
11+
type CFG,
12+
type GraphEdge,
13+
type GraphNode,
14+
mergeNodeAttrs,
15+
} from "../../control-flow/cfg-defs";
1116
import { simplifyCFG, trimFor } from "../../control-flow/graph-ops";
1217
import { Graphviz } from "@hpcc-js/wasm-graphviz";
1318
import { graphToDot } from "../../control-flow/render";
1419
import {
20+
type ColorScheme,
1521
getDarkColorList,
1622
getLightColorList,
1723
listToScheme,
1824
} from "../../control-flow/colors";
1925
import Panzoom, { type PanzoomObject } from "@panzoom/panzoom";
2026
import { onMount } from "svelte";
27+
import { MultiDirectedGraph } from "graphology";
28+
29+
let codeUrl: string | undefined;
2130
2231
/**
2332
* A reference to a function on GitHub
@@ -26,7 +35,7 @@
2635
/**
2736
* The URL for the raw file on GitHub
2837
*/
29-
rawURL: string;
38+
rawUrl: string;
3039
/**
3140
* The line-number for the function
3241
*/
@@ -45,12 +54,12 @@
4554
throw new Error("Missing line number.");
4655
}
4756
48-
const rawURL = githubURL.replace(
57+
const rawUrl = githubURL.replace(
4958
/(?<host>https:\/\/github.com\/)(?<project>\w+\/\w+\/)(blob\/)(?<path>.*)(#L\d+)/,
5059
"https://raw.githubusercontent.com/$<project>$<path>",
5160
);
5261
53-
return { line, rawURL };
62+
return { line, rawUrl };
5463
}
5564
5665
/**
@@ -106,30 +115,103 @@
106115
107116
let rawSVG: string | undefined;
108117
109-
async function render() {
110-
const urlSearchParams = new URLSearchParams(window.location.search);
111-
const githubUrl = urlSearchParams.get("github") ?? "";
118+
type GithubParams = {
119+
type: "GitHub";
120+
rawUrl: string;
121+
codeUrl: string;
122+
line: number;
123+
};
124+
type GraphParams = {
125+
type: "Graph";
126+
rawUrl: string;
127+
};
128+
type Params = (GithubParams | GraphParams) & {
129+
colorScheme: ColorScheme;
130+
colors: "light" | "dark";
131+
};
132+
133+
function parseUrlSearchParams(urlSearchParams: URLSearchParams): Params {
134+
const githubUrl = urlSearchParams.get("github");
112135
const colors = urlSearchParams.get("colors") ?? "dark";
113-
if (colors !== "light" && colors !== "dark") {
114-
throw new Error(`Unsupported color scheme ${colors}`);
136+
const graphUrl = urlSearchParams.get("graph");
137+
138+
if (colors !== "dark" && colors !== "light") {
139+
throw new Error("Invalid color scheme");
140+
}
141+
if (!(githubUrl || graphUrl)) {
142+
throw new Error("No URL provided");
115143
}
144+
if (githubUrl && graphUrl) {
145+
throw new Error("Too many URLs provided");
146+
}
147+
116148
const colorScheme = getColorScheme(colors);
117-
setBackgroundColor(colors);
118149
119-
const { line, rawURL } = parseGithubUrl(githubUrl);
120-
const response = await fetch(rawURL);
150+
if (githubUrl) {
151+
const { line, rawUrl } = parseGithubUrl(githubUrl);
152+
return { type: "GitHub", rawUrl, line, colorScheme, colors, codeUrl };
153+
}
154+
return {
155+
type: "Graph",
156+
rawUrl: graphUrl,
157+
colorScheme: colorScheme,
158+
colors,
159+
};
160+
}
161+
162+
async function createGitHubCFG(ghParams: GithubParams): Promise<CFG> {
163+
const { rawUrl, line } = ghParams;
164+
const response = await fetch(rawUrl);
121165
const code = await response.text();
122166
// We assume that the raw URL always ends with the file extension
123-
const language = getLanguage(rawURL);
167+
const language = getLanguage(rawUrl);
124168
125169
const func = await getFunctionByLine(code, language, line);
126170
if (!func) {
127171
throw new Error(`Unable to find function on line ${line}`);
128172
}
129173
130-
const cfg = buildCFG(func, language);
174+
return buildCFG(func, language);
175+
}
176+
177+
async function createGraphCFG(graphParams: GraphParams): Promise<CFG> {
178+
const { rawUrl } = graphParams;
179+
const response = await fetch(rawUrl);
180+
const jsonData = await response.json();
181+
const graph = new MultiDirectedGraph<GraphNode, GraphEdge>();
182+
graph.import(jsonData);
183+
184+
const entry = graph.findNode(
185+
(node, _attributes) => graph.inDegree(node) === 0,
186+
);
187+
if (!entry) {
188+
throw new Error("No entry found");
189+
}
190+
return { graph, entry, offsetToNode: [] };
191+
}
192+
193+
async function createCFG(params: Params): Promise<CFG> {
194+
switch (params.type) {
195+
case "GitHub":
196+
return createGitHubCFG(params);
197+
case "Graph":
198+
return createGraphCFG(params);
199+
}
200+
}
201+
202+
async function render() {
203+
const urlSearchParams = new URLSearchParams(window.location.search);
204+
const params = parseUrlSearchParams(urlSearchParams);
205+
setBackgroundColor(params.colors);
206+
if (params.type === "GitHub") {
207+
codeUrl = params.codeUrl;
208+
}
209+
210+
const cfg = await createCFG(params);
131211
const graphviz = await Graphviz.load();
132-
rawSVG = graphviz.dot(graphToDot(cfg, false, undefined, colorScheme));
212+
rawSVG = graphviz.dot(
213+
graphToDot(cfg, false, undefined, params.colorScheme),
214+
);
133215
return rawSVG;
134216
}
135217
@@ -150,12 +232,7 @@
150232
}
151233
152234
function openCode() {
153-
const urlSearchParams = new URLSearchParams(window.location.search);
154-
const githubUrl = urlSearchParams.get("github") ?? "";
155-
156-
if (!githubUrl) return;
157-
158-
window.open(githubUrl, "_blank").focus();
235+
window.open(codeUrl, "_blank").focus();
159236
}
160237
161238
function saveSVG() {
@@ -190,7 +267,12 @@
190267
<div class="controlsContainer">
191268
<div class="controls">
192269
<button on:click={resetView}>Reset View</button>
193-
<button on:click={openCode}>Open Code</button>
270+
<button
271+
on:click={openCode}
272+
disabled={!Boolean(codeUrl)}
273+
title={Boolean(codeUrl) ? "" : "Only available for GitHub code"}
274+
>Open Code</button
275+
>
194276
<button on:click={saveSVG}>Download SVG</button>
195277
</div>
196278
</div>

0 commit comments

Comments
 (0)