Skip to content

Commit

Permalink
Merge pull request #52 from tmr232/ghidra
Browse files Browse the repository at this point in the history
Add utils for rendering Ghidra graphs
  • Loading branch information
tmr232 authored Jan 4, 2025
2 parents 4dab470 + a4b26fc commit c440158
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 26 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how

- The JetBrains plugin can now change settings: flat switch, simplification, highlighting, and color scheme
- `/render` page to render code directly from GitHub, given a URL with a line number.
- `render-graph.ts` script to render a graph from a JSON file exported from code
- `/render` page can now render a graph provided to it directly.

### Fixed

- `detectBacklinks` no longer has infinite loops on specific cases, and is faster.

## [0.0.12] - 2024-12-18

Expand Down
51 changes: 51 additions & 0 deletions scripts/render-graph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { parseArgs } from "node:util";
import { Graphviz } from "@hpcc-js/wasm-graphviz";
import { MultiDirectedGraph } from "graphology";
import type {
CFG,
GraphEdge,
GraphNode,
} from "../src/control-flow/cfg-defs.ts";
import { graphToDot } from "../src/control-flow/render.ts";

async function main() {
const {
positionals: [_runtime, _this, gist_url],
} = parseArgs({
args: Bun.argv,
strict: true,
allowPositionals: true,
});

if (!gist_url) {
throw new Error("Missing URL");
}

const data = await (async () => {
if (gist_url.startsWith("http")) {
const response = await fetch(gist_url);
return response.json();
}
return Bun.file(gist_url).json();
})();

const graph = new MultiDirectedGraph<GraphNode, GraphEdge>();
graph.import(data);

const entry = graph.findNode(
(node, _attributes) => graph.inDegree(node) === 0,
);
if (!entry) {
throw new Error("No entry found");
}
const cfg: CFG = { graph, entry, offsetToNode: [] };
const dot = graphToDot(cfg);
const graphviz = await Graphviz.load();
const svg = graphviz.dot(dot);
console.log(svg);
// console.log(dot);
}

if (require.main === module) {
await main();
}
29 changes: 25 additions & 4 deletions src/control-flow/graph-ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,16 +131,37 @@ export function detectBacklinks(
entry: string,
): { from: string; to: string }[] {
const backlinks: { from: string; to: string }[] = [];
const stack: { node: string; path: string[] }[] = [{ node: entry, path: [] }];
// Using Set() for O(1) path lookups
const stack: { node: string; path: Set<string> }[] = [
{ node: entry, path: new Set<string>() },
];

const alreadyFound = (backlink: { from: string; to: string }): boolean => {
return backlinks.some(
(item) => item.from === backlink.from && item.to === backlink.to,
);
};

// If we ever visit a node that lead to a cycle, we will find the cycle.
// No need to revisit nodes from different paths.
const visited = new Set<string>();

let current = stack.pop();
for (; current !== undefined; current = stack.pop()) {
const { node, path } = current;
if (visited.has(node)) continue;
visited.add(node);
for (const child of graph.outNeighbors(node)) {
if (path.includes(child)) {
backlinks.push({ from: node, to: child });
// Self-loops must be explicitly checked because of the sequence of stack pushes
if (path.has(child) || child === node) {
// Only store backlinks once
const backlink = { from: node, to: child };
if (!alreadyFound(backlink)) {
backlinks.push(backlink);
}
continue;
}
stack.push({ node: child, path: [...path, node] });
stack.push({ node: child, path: new Set(path).add(node) });
}
}

Expand Down
126 changes: 104 additions & 22 deletions src/render/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,26 @@
import type Parser from "web-tree-sitter";
import { type SyntaxNode } from "web-tree-sitter";
import { type Language, newCFGBuilder } from "../../control-flow/cfg";
import { type CFG, mergeNodeAttrs } from "../../control-flow/cfg-defs";
import {
type CFG,
type GraphEdge,
type GraphNode,
mergeNodeAttrs,
} from "../../control-flow/cfg-defs";
import { simplifyCFG, trimFor } from "../../control-flow/graph-ops";
import { Graphviz } from "@hpcc-js/wasm-graphviz";
import { graphToDot } from "../../control-flow/render";
import {
type ColorScheme,
getDarkColorList,
getLightColorList,
listToScheme,
} from "../../control-flow/colors";
import Panzoom, { type PanzoomObject } from "@panzoom/panzoom";
import { onMount } from "svelte";
import { MultiDirectedGraph } from "graphology";
let codeUrl: string | undefined;
/**
* A reference to a function on GitHub
Expand All @@ -26,7 +35,7 @@
/**
* The URL for the raw file on GitHub
*/
rawURL: string;
rawUrl: string;
/**
* The line-number for the function
*/
Expand All @@ -45,12 +54,12 @@
throw new Error("Missing line number.");
}
const rawURL = githubURL.replace(
const rawUrl = githubURL.replace(
/(?<host>https:\/\/github.com\/)(?<project>\w+\/\w+\/)(blob\/)(?<path>.*)(#L\d+)/,
"https://raw.githubusercontent.com/$<project>$<path>",
);
return { line, rawURL };
return { line, rawUrl };
}
/**
Expand Down Expand Up @@ -106,30 +115,103 @@
let rawSVG: string | undefined;
async function render() {
const urlSearchParams = new URLSearchParams(window.location.search);
const githubUrl = urlSearchParams.get("github") ?? "";
type GithubParams = {
type: "GitHub";
rawUrl: string;
codeUrl: string;
line: number;
};
type GraphParams = {
type: "Graph";
rawUrl: string;
};
type Params = (GithubParams | GraphParams) & {
colorScheme: ColorScheme;
colors: "light" | "dark";
};
function parseUrlSearchParams(urlSearchParams: URLSearchParams): Params {
const githubUrl = urlSearchParams.get("github");
const colors = urlSearchParams.get("colors") ?? "dark";
if (colors !== "light" && colors !== "dark") {
throw new Error(`Unsupported color scheme ${colors}`);
const graphUrl = urlSearchParams.get("graph");
if (colors !== "dark" && colors !== "light") {
throw new Error("Invalid color scheme");
}
if (!(githubUrl || graphUrl)) {
throw new Error("No URL provided");
}
if (githubUrl && graphUrl) {
throw new Error("Too many URLs provided");
}
const colorScheme = getColorScheme(colors);
setBackgroundColor(colors);
const { line, rawURL } = parseGithubUrl(githubUrl);
const response = await fetch(rawURL);
if (githubUrl) {
const { line, rawUrl } = parseGithubUrl(githubUrl);
return { type: "GitHub", rawUrl, line, colorScheme, colors, codeUrl };
}
return {
type: "Graph",
rawUrl: graphUrl,
colorScheme: colorScheme,
colors,
};
}
async function createGitHubCFG(ghParams: GithubParams): Promise<CFG> {
const { rawUrl, line } = ghParams;
const response = await fetch(rawUrl);
const code = await response.text();
// We assume that the raw URL always ends with the file extension
const language = getLanguage(rawURL);
const language = getLanguage(rawUrl);
const func = await getFunctionByLine(code, language, line);
if (!func) {
throw new Error(`Unable to find function on line ${line}`);
}
const cfg = buildCFG(func, language);
return buildCFG(func, language);
}
async function createGraphCFG(graphParams: GraphParams): Promise<CFG> {
const { rawUrl } = graphParams;
const response = await fetch(rawUrl);
const jsonData = await response.json();
const graph = new MultiDirectedGraph<GraphNode, GraphEdge>();
graph.import(jsonData);
const entry = graph.findNode(
(node, _attributes) => graph.inDegree(node) === 0,
);
if (!entry) {
throw new Error("No entry found");
}
return { graph, entry, offsetToNode: [] };
}
async function createCFG(params: Params): Promise<CFG> {
switch (params.type) {
case "GitHub":
return createGitHubCFG(params);
case "Graph":
return createGraphCFG(params);
}
}
async function render() {
const urlSearchParams = new URLSearchParams(window.location.search);
const params = parseUrlSearchParams(urlSearchParams);
setBackgroundColor(params.colors);
if (params.type === "GitHub") {
codeUrl = params.codeUrl;
}
const cfg = await createCFG(params);
const graphviz = await Graphviz.load();
rawSVG = graphviz.dot(graphToDot(cfg, false, undefined, colorScheme));
rawSVG = graphviz.dot(
graphToDot(cfg, false, undefined, params.colorScheme),
);
return rawSVG;
}
Expand All @@ -150,12 +232,7 @@
}
function openCode() {
const urlSearchParams = new URLSearchParams(window.location.search);
const githubUrl = urlSearchParams.get("github") ?? "";
if (!githubUrl) return;
window.open(githubUrl, "_blank").focus();
window.open(codeUrl, "_blank").focus();
}
function saveSVG() {
Expand Down Expand Up @@ -190,7 +267,12 @@
<div class="controlsContainer">
<div class="controls">
<button on:click={resetView}>Reset View</button>
<button on:click={openCode}>Open Code</button>
<button
on:click={openCode}
disabled={!Boolean(codeUrl)}
title={Boolean(codeUrl) ? "" : "Only available for GitHub code"}
>Open Code</button
>
<button on:click={saveSVG}>Download SVG</button>
</div>
</div>
Expand Down

0 comments on commit c440158

Please sign in to comment.