├── .gitattributes
├── .github
└── workflows
│ ├── gradle.yml
│ ├── release.yml
│ └── test-kernel-config.json
├── .gitignore
├── .idea
├── fileTemplates
│ └── GhidraKotlinScript.kt
└── runConfigurations
│ └── Ghidra_GUI.xml
├── GhidraJupyterKotlin
├── .gitignore
├── Module.manifest
├── build.gradle
├── extension.properties
├── ghidra_scripts
│ ├── Dumpx64dbgLabels.kt
│ └── HelloWorldScriptKt.kt
├── gradle.properties
├── lib
│ └── .gitkeep
└── src
│ └── main
│ ├── java
│ ├── GhidraJupyterKotlin
│ │ ├── CellContext.java
│ │ ├── ConnectionFile.java
│ │ ├── GhidraKotlinKernelLaunchable.java
│ │ ├── InterruptKernelAction.kt
│ │ ├── JupyterKotlinPlugin.java
│ │ ├── KernelThread.java
│ │ ├── KotlinQtConsoleThread.java
│ │ ├── LineReader.java
│ │ ├── NotebookProxy.java
│ │ ├── NotebookThread.java
│ │ ├── ShutDownKernelAction.kt
│ │ └── extensions
│ │ │ ├── address
│ │ │ └── AddressExtensions.kt
│ │ │ ├── data
│ │ │ └── DataExtensions.kt
│ │ │ └── misc
│ │ │ └── MiscExtensions.kt
│ └── ghidra
│ │ └── app
│ │ └── script
│ │ ├── KotlinCompilerMessageCollector.kt
│ │ └── KotlinScriptProvider.java
│ └── resources
│ └── images
│ ├── README.txt
│ ├── notebook.png
│ └── qtconsole.png
├── LICENSE
├── README.md
├── ghidra_jupyter
├── kernel
│ ├── kernel.css
│ ├── kernel.js
│ ├── kernel.json
│ ├── logo-16x16.png
│ ├── logo-64x64.png
│ ├── wavyline-orange.gif
│ └── wavyline-red.gif
├── requirements.txt
├── setup.py
└── src
│ └── ghidra_jupyter
│ ├── __init__.py
│ ├── dispatcher.py
│ └── installer.py
├── gradle.properties
├── resources
├── notebook-button.svg
├── notebook-logo.svg
├── options.png
├── qtconsole-button.svg
└── readme
│ ├── buttons.png
│ ├── create_notebook.png
│ ├── interrupt_demo.png
│ ├── menu.png
│ ├── notebook.png
│ ├── notebook_view.png
│ ├── qtconsole.png
│ ├── qtconsole_window.png
│ └── waiting.png
└── settings.gradle
/.gitattributes:
--------------------------------------------------------------------------------
1 | ghidra_jupyter/kernel/* linguist-vendored
2 |
--------------------------------------------------------------------------------
/.github/workflows/gradle.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a Java project with Gradle
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
3 |
4 | name: Test
5 |
6 | on:
7 | pull_request:
8 | branches: [ main ]
9 |
10 | jobs:
11 | build:
12 |
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: Set up JDK 1.21
18 | uses: actions/setup-java@v1
19 | with:
20 | java-version: 1.21
21 | - uses: er28-0652/setup-ghidra@master
22 | with:
23 | version: "11.3"
24 |
25 | - name: Build Extension
26 | working-directory: ./GhidraJupyterKotlin
27 | run: gradle buildExtension
28 |
29 | - name: Upload built extension as artifact for debugging
30 | uses: actions/upload-artifact@v4
31 | with:
32 | path: ./GhidraJupyterKotlin/dist/*zip
33 | retention-days: 1
34 |
35 | - name: Install Extension
36 | run: unzip ./GhidraJupyterKotlin/dist/*zip -d $GHIDRA_INSTALL_DIR/Ghidra/Extensions
37 |
38 | - name: Test Kernel Startup # Test if the kernel manages to not crash before the timeout terminates it
39 | run: timeout 5s $GHIDRA_INSTALL_DIR/support/launch.sh fg jdk Ghidra '' '' GhidraJupyterKotlin.GhidraKotlinKernelLaunchable .github/workflows/test-kernel-config.json || exit $(($?-124))
40 |
41 | - name: Install jupyter-console for testing evaluation
42 | run: sudo apt-get install jupyter-console
43 |
44 | - name: Test basic evaluation
45 | run: |
46 | echo "1+1" | jupyter-console --existing=.github/workflows/test-kernel-config.json &
47 | timeout 10s $GHIDRA_INSTALL_DIR/support/launch.sh fg jdk Ghidra '' '' GhidraJupyterKotlin.GhidraKotlinKernelLaunchable .github/workflows/test-kernel-config.json || exit $(($?-124))
48 |
49 | - name: Test support for .kt Scripts
50 | run: |
51 | $GHIDRA_INSTALL_DIR/support/analyzeHeadless /tmp tmp -readonly -preScript $GITHUB_WORKSPACE/GhidraJupyterKotlin/ghidra_scripts/HelloWorldScriptKt.kt | grep "HelloWorldScriptKt.kt> Hello in Kotlin from null! (GhidraScript)"
52 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Main
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*.*.*'
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - name: Set up JDK 1.21
14 | uses: actions/setup-java@v1
15 | with:
16 | java-version: 1.21
17 | - uses: er28-0652/setup-ghidra@master
18 | with:
19 | version: "11.3"
20 |
21 | - name: Build with Gradle
22 | working-directory: ./GhidraJupyterKotlin
23 | run: gradle buildExtension
24 |
25 | - name: Debug github.ref
26 | run: echo ${{github.ref}}
27 |
28 | - name: Release
29 | uses: softprops/action-gh-release@v2
30 | with:
31 | files: ./GhidraJupyterKotlin/dist/*zip
32 | env:
33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34 |
--------------------------------------------------------------------------------
/.github/workflows/test-kernel-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "shell_port": 38477,
3 | "iopub_port": 58089,
4 | "stdin_port": 33185,
5 | "control_port": 35869,
6 | "hb_port": 51201,
7 | "ip": "127.0.0.1",
8 | "key": "42dcd222-2548fa07db7fec5eaf72e22b",
9 | "transport": "tcp",
10 | "signature_scheme": "hmac-sha256",
11 | "kernel_name": "ghidra-kotlin"
12 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | gradle/
3 | *.class
4 | META-INF
5 | ghidra_jupyter/build/
6 | ghidra_jupyter/dist/
7 |
--------------------------------------------------------------------------------
/.idea/fileTemplates/GhidraKotlinScript.kt:
--------------------------------------------------------------------------------
1 | // SCRIPT DESCRIPTION
2 | //@category Examples
3 | //@toolbar world.png
4 |
5 | import ghidra.app.script.GhidraScript
6 | import GhidraJupyterKotlin.extensions.address.*
7 | import GhidraJupyterKotlin.extensions.data.*
8 | import GhidraJupyterKotlin.extensions.misc.*
9 |
10 | @Suppress("unused")
11 | class ${NAME} : GhidraScript() {
12 | @Throws(Exception::class)
13 | override fun run() {
14 | TODO("Script code goes here")
15 | }
16 | }
--------------------------------------------------------------------------------
/.idea/runConfigurations/Ghidra_GUI.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/.gitignore:
--------------------------------------------------------------------------------
1 | lib/
2 | build/
3 | dist/
4 | .gradle/
5 |
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/Module.manifest:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/GhidraJupyterKotlin/Module.manifest
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/build.gradle:
--------------------------------------------------------------------------------
1 | // Builds a Ghidra Extension for a given Ghidra installation.
2 | //
3 | // An absolute path to the Ghidra installation directory must be supplied either by setting the
4 | // GHIDRA_INSTALL_DIR environment variable or Gradle project property:
5 | //
6 | // > export GHIDRA_INSTALL_DIR=
7 | // > gradle
8 | //
9 | // or
10 | //
11 | // > gradle -PGHIDRA_INSTALL_DIR=
12 | //
13 | // Gradle should be invoked from the directory of the project to build. Please see the
14 | // application.gradle.version property in /Ghidra/application.properties
15 | // for the correction version of Gradle to use for the Ghidra installation you specify.
16 |
17 | plugins {
18 | id 'org.jetbrains.kotlin.jvm' version "$kotlinVersion"
19 | id 'idea'
20 | }
21 |
22 |
23 | compileKotlin {
24 | kotlinOptions.jvmTarget = "21"
25 | }
26 |
27 | kotlin {
28 | jvmToolchain(21)
29 | }
30 |
31 | configurations {
32 | requiredLibs //.extendsFrom implementation
33 | }
34 |
35 |
36 | repositories {
37 | flatDir {
38 | dirs 'lib'
39 | }
40 | mavenCentral()
41 |
42 | }
43 |
44 | //----------------------START "DO NOT MODIFY" SECTION------------------------------
45 | def ghidraInstallDir
46 |
47 | if (System.env.GHIDRA_INSTALL_DIR) {
48 | ghidraInstallDir = System.env.GHIDRA_INSTALL_DIR
49 | }
50 | else if (project.hasProperty("GHIDRA_INSTALL_DIR")) {
51 | ghidraInstallDir = project.getProperty("GHIDRA_INSTALL_DIR")
52 | }
53 |
54 | if (ghidraInstallDir) {
55 | apply from: new File(ghidraInstallDir).getCanonicalPath() + "/support/buildExtension.gradle"
56 | }
57 | else {
58 | throw new GradleException("GHIDRA_INSTALL_DIR is not defined!")
59 | }
60 | //----------------------END "DO NOT MODIFY" SECTION-------------------------------
61 |
62 | dependencies {
63 | api('org.jetbrains.kotlinx:kotlin-jupyter-kernel:0.12.0-356'){
64 | // exclude group: "org.slf4j", module: "slf4j-api"
65 | }
66 | // Needed for compiling
67 | // after building they are already included as part of the recursive dependencies of kotlin-jupyter-kernel
68 | api group: 'org.jetbrains.kotlin', name: 'kotlin-compiler-embeddable', version: "$kotlinVersion"
69 | // api group: 'org.json', name: 'json', version: '20210307'
70 | api group: 'org.zeromq', name: 'jeromq', version: '0.6.0'
71 | // implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1"
72 | }
73 | compileKotlin.dependsOn(copyDependencies)
74 | buildExtension.dependsOn(copyDependencies)
75 |
76 | sourceSets {
77 | main {
78 | kotlin {
79 | srcDirs 'ghidra_scripts'
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/extension.properties:
--------------------------------------------------------------------------------
1 | name=@extname@
2 | description=Kotlin Jupyter kernel for Ghidra.
3 | author=GhidraJupyter
4 | createdOn=2020-12-06
5 | version=@extversion@
6 |
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/ghidra_scripts/Dumpx64dbgLabels.kt:
--------------------------------------------------------------------------------
1 | // Generate a x64dbg script based on the currentProgram that labels all the functions in x64dbg and stores it in the clipboard
2 | //@category Debugger
3 |
4 | import ghidra.app.script.GhidraScript
5 | import GhidraJupyterKotlin.extensions.misc.*
6 | import java.awt.Toolkit
7 | import java.awt.datatransfer.StringSelection
8 |
9 |
10 | @Suppress("unused")
11 | class Dumpx64dbgLabels : GhidraScript() {
12 | @Throws(Exception::class)
13 | override fun run() {
14 | currentProgram.functions
15 | .map { f -> "lblset 0x${f.entryPoint.offset}, ${f.name}"}
16 | .joinToString("\n")
17 | .let {
18 | val sel = StringSelection(it)
19 | Toolkit.getDefaultToolkit().systemClipboard.setContents(sel, sel)
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/ghidra_scripts/HelloWorldScriptKt.kt:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | //Writes "Hello, Kotlin!" to console.
17 | //@category Examples
18 | //@menupath Help.Examples.Hello World (Kotlin)
19 | //@toolbar world.png
20 |
21 | import ghidra.app.script.GhidraScript
22 |
23 | @Suppress("unused")
24 | class HelloWorldScriptKt : GhidraScript() {
25 | @Throws(Exception::class)
26 | override fun run() {
27 | println("Hello in Kotlin from $currentProgram!")
28 | }
29 | }
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlinVersion=1.9.23
2 |
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/lib/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/GhidraJupyterKotlin/lib/.gitkeep
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/CellContext.java:
--------------------------------------------------------------------------------
1 | package GhidraJupyterKotlin;
2 |
3 | import ghidra.app.script.GhidraScript;
4 | import ghidra.program.model.address.Address;
5 | import ghidra.program.model.listing.Function;
6 | import ghidra.program.util.ProgramLocation;
7 | import ghidra.program.util.ProgramSelection;
8 |
9 | public class CellContext extends GhidraScript {
10 |
11 | ProgramLocation currentContextLocation;
12 |
13 | public CellContext(GhidraScript script) {
14 | super();
15 | this.set(script.getState(),
16 | script.getMonitor(),
17 | null
18 | );
19 | }
20 |
21 | public CellContext() {
22 | super();
23 | }
24 |
25 |
26 |
27 | @Override
28 | protected void run() throws Exception {
29 | throw new Exception("This is not supposed to be run as a script!");
30 | }
31 |
32 | @Override
33 | public void print(String message) {
34 | super.print(message);
35 | System.out.print(message);
36 | }
37 |
38 | @Override
39 | public void println(String message) {
40 | super.println(message);
41 | System.out.println(message);
42 | }
43 |
44 | // Expose protected variables with a getter
45 | // Kotlin will allow accessing them without the get prefix
46 | // e.g. currentAddress in the Jupyter console will call this function and not return currentAddress directly
47 | // This also allows adding some extra useful helpers like currentFunction
48 |
49 | public Address getCurrentAddress() {
50 | return currentAddress;
51 | }
52 |
53 | // This is not the same as the currentLocation in the Jython Shell or Ghidra Scripts
54 | // This uses the object provided to GhidraJupyterKotlin.JupyterKotlinPlugin.locationChanged directly
55 | // This contains more information like the exact token referenced in the decompiler or disassembly listing
56 | // The normal currentLocation in the Jython shell seems to be always simply the current address which is less useful
57 | public ProgramLocation getCurrentLocation() {
58 | return currentContextLocation;
59 | }
60 |
61 | public ProgramSelection getCurrentSelection() {
62 | return currentSelection;
63 | }
64 |
65 | public ProgramSelection getCurrentHighlight() {
66 | return currentHighlight;
67 | }
68 |
69 | public Function getCurrentFunction() {
70 | return currentProgram.getFunctionManager().getFunctionContaining(currentAddress);
71 | }
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/ConnectionFile.java:
--------------------------------------------------------------------------------
1 | package GhidraJupyterKotlin;
2 |
3 |
4 | import ghidra.util.Msg;
5 |
6 | import java.io.BufferedReader;
7 | import java.io.BufferedWriter;
8 | import java.io.File;
9 | import java.io.FileWriter;
10 | import java.io.IOException;
11 | import java.io.InputStreamReader;
12 | import java.net.InetSocketAddress;
13 | import java.net.Socket;
14 | import java.nio.file.Files;
15 | import java.util.ArrayList;
16 | import java.util.List;
17 | import java.util.UUID;
18 |
19 | public class ConnectionFile {
20 |
21 | public static File create() {
22 | return writeConfigFile();
23 |
24 | }
25 |
26 | private static List getPorts(Integer n) {
27 | ArrayList sockets = new ArrayList<>();
28 | try {
29 | for (int i = 0; i < n; i++) {
30 | Socket s = new Socket();
31 | s.setSoLinger(false, 0);
32 | s.bind(new InetSocketAddress("", 0));
33 | sockets.add(s);
34 | }
35 | } catch (IOException e) {
36 | e.printStackTrace();
37 | return null;
38 | }
39 | ArrayList ports = new ArrayList<>();
40 | for (Socket s : sockets) {
41 | ports.add(s.getLocalPort());
42 | try {
43 | s.close();
44 | } catch (IOException e) {
45 | e.printStackTrace();
46 | }
47 | }
48 |
49 | return ports;
50 | }
51 |
52 |
53 | private static String getJupyterRuntime() {
54 | ProcessBuilder builder = new ProcessBuilder("jupyter", "--runtime-dir");
55 | try {
56 |
57 | Process process = builder.start();
58 | process.waitFor();
59 |
60 | BufferedReader processOutputReader = new BufferedReader(
61 | new InputStreamReader(process.getInputStream()));
62 |
63 | return processOutputReader.readLine().strip();
64 | } catch (InterruptedException | IOException e) {
65 | throw new RuntimeException("Could not get jupyter runtime directory, please report this issue",e);
66 | }
67 | }
68 |
69 |
70 | private static String formatConnectionFile(String key, List ports) {
71 | return String.format("{\n" +
72 | " \"control_port\": %d,\n" +
73 | " \"shell_port\": %d,\n" +
74 | " \"transport\": \"tcp\",\n" +
75 | " \"signature_scheme\": \"hmac-sha256\",\n" +
76 | " \"stdin_port\": %d,\n" +
77 | " \"hb_port\": %d,\n" +
78 | " \"ip\": \"127.0.0.1\",\n" +
79 | " \"iopub_port\": %d,\n" +
80 | " \"key\": \"%s\"\n" +
81 | "}", ports.get(0), ports.get(1), ports.get(2), ports.get(3), ports.get(4), key);
82 | }
83 |
84 | private static File writeConfigFile() {
85 | String key = UUID.randomUUID().toString();
86 | List ports = getPorts(5);
87 | if (ports == null) {
88 | return null;
89 | }
90 | String runtimeDir = getJupyterRuntime();
91 | File kernelFile = new File(runtimeDir, String.format("kernel-%s.json", key));
92 | // Make sure that the directory actually exists
93 | // this is not guaranteed by only invoking `jupyter --runtime-dir`
94 | // on new machines that never did anything with jupyter the directory won't exist
95 | // and writing the file will fail
96 | Msg.info(ConnectionFile.class, "Trying to write Kernel file to: " + kernelFile.getAbsolutePath());
97 | if (kernelFile.getParentFile() == null) {
98 | throw new RuntimeException("Parent directory of kernel file %s is unexpectedly null, please report this issue".formatted(kernelFile.toString()));
99 | }
100 | try {
101 | Files.createDirectories(kernelFile.getParentFile().toPath());
102 | } catch (IOException e) {
103 | e.printStackTrace();
104 | throw new RuntimeException(e);
105 | }
106 |
107 | String connectionFile = formatConnectionFile(key, ports);
108 |
109 | try {
110 | BufferedWriter writer = new BufferedWriter(new FileWriter(kernelFile));
111 | writer.write(connectionFile);
112 | writer.flush();
113 | writer.close();
114 | } catch (IOException e) {
115 | e.printStackTrace();
116 | return null;
117 | }
118 |
119 | return kernelFile;
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/GhidraKotlinKernelLaunchable.java:
--------------------------------------------------------------------------------
1 | package GhidraJupyterKotlin;
2 |
3 | import ghidra.GhidraApplicationLayout;
4 | import ghidra.GhidraLaunchable;
5 | import ghidra.util.Msg;
6 | import org.jetbrains.kotlinx.jupyter.IkotlinKt;
7 | import org.jetbrains.kotlinx.jupyter.libraries.EmptyResolutionInfoProvider;
8 |
9 | import java.io.File;
10 | import java.util.Collections;
11 |
12 | public class GhidraKotlinKernelLaunchable implements GhidraLaunchable {
13 | @Override
14 | public void launch(GhidraApplicationLayout ghidraApplicationLayout, String[] args) throws Exception {
15 | var connectionFile = new File(args[0]);
16 | Msg.info(this, connectionFile.toString());
17 | IkotlinKt.embedKernel(
18 | connectionFile,
19 | null,
20 | null);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/InterruptKernelAction.kt:
--------------------------------------------------------------------------------
1 | //package GhidraJupyterKotlin
2 | //
3 | //import docking.ActionContext
4 | //import docking.action.DockingAction
5 | //import org.jetbrains.kotlinx.jupyter.*
6 | //import org.jetbrains.kotlinx.jupyter.messaging.*
7 | //import org.jetbrains.kotlinx.jupyter.startup.KernelJupyterParams
8 | //import org.zeromq.ZMQ
9 | //import java.util.*
10 | //
11 | //
12 | //class InterruptKernelAction(var parentPlugin: JupyterKotlinPlugin): DockingAction("Interrupt Kernel", parentPlugin.name) {
13 | // override fun actionPerformed(context: ActionContext?) {
14 | // // This creates a ZMQ socket, to send an interrupt request to the running kernel
15 | // // This is needed because Jupyter QT Console doesn't seem to provide this feature and just prints
16 | // // "Cannot interrupt a kernel I did not start"
17 | // // when using the "Interrupt Kernel" menu entry
18 | //
19 | // val config: KernelJupyterParams = KernelJupyterParams.fromFile(parentPlugin.connectionFile)
20 | //
21 | // val ctx = ZMQ.context(1)
22 | // val control = ctx.socket(JupyterSockets.CONTROL.zmqClientType)
23 | // control.connect("${config.transport}://*:${config.ports[JupyterSockets.CONTROL.ordinal]}")
24 | // if (config.sigScheme == "hmac-sha256"){
25 | // val hmac = HMAC("HmacSHA256", config.key!!)
26 | // control.sendMessage(
27 | // Message(id = listOf(byteArrayOf(1)),
28 | // MessageData(
29 | // header = makeHeader(
30 | // MessageType.INTERRUPT_REQUEST,
31 | // sessionId = UUID.randomUUID().toString()),
32 | // content = InterruptRequest())),
33 | // hmac)
34 | // }
35 | //
36 | //
37 | // }
38 | //}
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/JupyterKotlinPlugin.java:
--------------------------------------------------------------------------------
1 | package GhidraJupyterKotlin;
2 |
3 | import docking.ActionContext;
4 | import docking.action.DockingAction;
5 | import docking.action.MenuData;
6 | import docking.action.ToolBarData;
7 | import docking.widgets.OptionDialog;
8 | import ghidra.app.plugin.PluginCategoryNames;
9 | import ghidra.app.plugin.ProgramPlugin;
10 | import ghidra.app.script.GhidraState;
11 | import ghidra.framework.options.OptionType;
12 | import ghidra.framework.options.Options;
13 | import ghidra.framework.plugintool.PluginInfo;
14 | import ghidra.framework.plugintool.PluginTool;
15 | import ghidra.framework.plugintool.util.PluginStatus;
16 | import ghidra.program.model.listing.Program;
17 | import ghidra.program.util.ProgramLocation;
18 | import ghidra.program.util.ProgramSelection;
19 | import ghidra.util.Msg;
20 | import ghidra.util.task.RunManager;
21 | import ghidra.util.task.TaskMonitor;
22 | import org.apache.commons.lang3.ArrayUtils;
23 | import resources.ResourceManager;
24 |
25 | import javax.swing.*;
26 | import java.awt.*;
27 | import java.io.BufferedReader;
28 | import java.io.File;
29 | import java.io.IOException;
30 | import java.io.InputStreamReader;
31 | import java.net.URI;
32 | import java.net.URISyntaxException;
33 |
34 | //@formatter:off
35 | @PluginInfo(
36 | status = PluginStatus.RELEASED,
37 | packageName = "GhidraJupyterKotlin",
38 | category = PluginCategoryNames.COMMON,
39 | shortDescription = "Kotlin Jupyter kernel for Ghidra.",
40 | description = "Kotlin Jupyter kernel for Ghidra."
41 | )
42 | //@formatter:on
43 | public class JupyterKotlinPlugin extends ProgramPlugin {
44 | private static final String OPTION_LAST_URI = "LAST_URI";
45 | private static final String DEFAULT_URI = "http://localhost:8888/tree";
46 | private static final String OPTION_CONSOLE_CMD = "CONSOLE_CMD";
47 | private static final String DEFAULT_CONSOLE_CMD = "jupyter-qtconsole --existing";
48 | private static final String PLUGIN_NAME = "JupyterKotlinPlugin";
49 | private final RunManager runManager = new RunManager();
50 | private final CellContext cellContext = new CellContext();
51 | private Options programOptions;
52 | private Options toolOptions;
53 |
54 | public File getConnectionFile() {
55 | return (currentKernel != null) ? currentKernel.getConnectionFile() : null;
56 | }
57 |
58 | private KernelThread currentKernel = null;
59 | /**
60 | * Plugin constructor.
61 | *
62 | * @param tool The plugin tool that this plugin is added to.
63 | */
64 | public JupyterKotlinPlugin(PluginTool tool) {
65 | super(tool);
66 | toolOptions = tool.getOptions(PLUGIN_NAME);
67 | toolOptions.registerOption(OPTION_CONSOLE_CMD, OptionType.STRING_TYPE, DEFAULT_CONSOLE_CMD, null,
68 | "Default Console command to execute (connection file will be appended)");
69 | toolOptions.registerOption(OPTION_LAST_URI, OptionType.STRING_TYPE, DEFAULT_URI, null,
70 | "Default URI to open when using the GUI shortcut. " +
71 | "This can be set to the full path to a specific notebook " +
72 | "that should open directly after the kernel starts waiting");
73 | registerActions();
74 | }
75 |
76 | public void clearKernel() {
77 | currentKernel = null;
78 | }
79 |
80 |
81 | private void registerActions(){
82 | DockingAction action = new DockingAction("Kotlin QtConsole", getName()) {
83 | @Override
84 | public void actionPerformed(ActionContext context) {
85 | if (getConnectionFile() == null) {
86 | currentKernel = new KotlinQtConsoleThread(cellContext, ConnectionFile.create());
87 | runManager.runNow(currentKernel, "Kotlin kernel");
88 | }
89 | launchQtConsole();
90 | }
91 | };
92 | ImageIcon qtconsoleIcon = ResourceManager.loadImage("images/qtconsole.png");
93 | action.setToolBarData(new ToolBarData(qtconsoleIcon, null));
94 | action.setMenuBarData(
95 | new MenuData(new String[] { "Jupyter", "Open QTConsole" }, qtconsoleIcon, null));
96 | action.setEnabled(true);
97 | action.markHelpUnnecessary();
98 | tool.addAction(action);
99 |
100 | DockingAction notebookAction = new DockingAction("Kotlin Notebook", getName()) {
101 | @Override
102 | public void actionPerformed(ActionContext context) {
103 | currentKernel = new NotebookThread(cellContext, tool);
104 | runManager.runNow(currentKernel, "Notebook");
105 | }
106 | };
107 | ImageIcon kernelIcon = ResourceManager.loadImage("images/notebook.png");
108 | notebookAction.setToolBarData(new ToolBarData(kernelIcon, null));
109 | notebookAction.setMenuBarData(
110 | new MenuData(new String[] { "Jupyter", "Start Kotlin Kernel for Notebook/Lab" }, kernelIcon, null));
111 | notebookAction.setEnabled(true);
112 | notebookAction.markHelpUnnecessary();
113 | tool.addAction(notebookAction);
114 |
115 |
116 | DockingAction serverAction = new DockingAction("Jupyter Server", getName()) {
117 | @Override
118 | public void actionPerformed(ActionContext context) {
119 | openNotebookServer();
120 | }
121 | };
122 |
123 | serverAction.setMenuBarData(
124 | new MenuData(new String[] { "Jupyter", "Open Jupyter Notebook Server" }, null, null));
125 | serverAction.setDescription("Tries to open existing server or offers to start a new one");
126 | tool.addAction(serverAction);
127 |
128 | DockingAction defaultNotebookAction = new DockingAction("Default Notebook", getName()) {
129 | @Override
130 | public void actionPerformed(ActionContext context) {
131 | openDefaultNotebook();
132 | }
133 | };
134 |
135 | defaultNotebookAction.setMenuBarData(
136 | new MenuData(new String[] { "Jupyter", "Open Default Notebook" }, null, null));
137 | defaultNotebookAction.setDescription("Open Default Notebook");
138 | tool.addAction(defaultNotebookAction);
139 |
140 | // DockingAction interruptAction = new InterruptKernelAction(this);
141 | // interruptAction.setMenuBarData(
142 | // new MenuData(new String[] { "Jupyter", "Interrupt Execution" }, null, null));
143 | // interruptAction.setDescription("Interrupts the currently running kernel if it is executing something");
144 | // tool.addAction(interruptAction);
145 |
146 | // DockingAction shutdownAction = new ShutDownKernelAction(this);
147 | // shutdownAction.setMenuBarData(
148 | // new MenuData(new String[] { "Jupyter", "Shutdown Kernel" }, null, null));
149 | // shutdownAction.setDescription("Terminates the currently running kernel if it isn't busy");
150 | // tool.addAction(shutdownAction);
151 | }
152 |
153 | private void launchQtConsole() {
154 | String[] console = toolOptions.getString(OPTION_CONSOLE_CMD,
155 | DEFAULT_CONSOLE_CMD).split(" ");
156 | String[] command = ArrayUtils.add(console, currentKernel.getConnectionFile().toString());
157 | try {
158 | Runtime.getRuntime().exec(command);
159 | } catch (IOException e) {
160 | Msg.showError(this, null, "QT Console process failed",
161 | "The console command failed to start because of an IOException.\n" +
162 | "Most likely jupyter-qtconsole is not available in your PATH because it wasn't installed\n" +
163 | "or your custom command has some issues\n" +
164 | "You can manually run the following command to debug this: \n" +
165 | String.join(" ", command) +
166 | "\nThe kernel*.json path is optional. Leaving it out will reconnect to your most recent running kernel, which is most likely the correct one.\n" +
167 | "You can also run 'jupyter-console --existing' for a terminal based console which is typically already included with a Jupyter install",
168 | e);
169 | }
170 | }
171 |
172 | private URI checkForRunningNotebookServer(){
173 | Runtime rt = Runtime.getRuntime();
174 | String[] commands = {"jupyter", "notebook", "list", "--json"};
175 |
176 | Process proc = null;
177 | try {
178 | proc = rt.exec(commands);
179 |
180 | BufferedReader stdInput = new BufferedReader(new
181 | InputStreamReader(proc.getInputStream()));
182 |
183 | String s = stdInput.readLine();
184 |
185 | // if (s != null) {
186 | // JSONObject obj = new JSONObject(s);
187 | // return new URI((String) obj.get("url"));
188 | // }
189 | } catch (IOException e) {
190 | e.printStackTrace();
191 | // } catch (URISyntaxException e) {
192 | // e.printStackTrace();
193 | }
194 | return null;
195 | }
196 |
197 | private void openURI(URI uri){
198 | if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
199 | try {
200 | Desktop.getDesktop().browse(uri);
201 | } catch (IOException e) {
202 | e.printStackTrace();
203 | }
204 | } else {
205 | Msg.error(this,
206 | "Notebook couldn't be opened because environment doesn't support opening URLs");
207 | }
208 | }
209 | private void openNotebookServer() {
210 | URI uri = checkForRunningNotebookServer();
211 |
212 | if (uri !=null){
213 | openURI(uri);
214 | } else {
215 | if (OptionDialog.showYesNoDialog(null,
216 | "Start new Jupyter server?",
217 | "No running Jupyter Notebook server could be detected, would you like to start a new one?\n" +
218 | "This server will use the default options by simply invoking 'jupyter-notebook'" +
219 | "and persist after closing Ghidra")
220 | == OptionDialog.OPTION_ONE){
221 | Runtime rt = Runtime.getRuntime();
222 | String[] commands = {"jupyter-notebook" };
223 | try {
224 | rt.exec(commands);
225 | } catch (IOException e) {
226 | Msg.showError(this, null,
227 | "Failed to start Jupyter Server",
228 | "The Jupyter Server could not be started", e);
229 | }
230 | }
231 |
232 | }
233 | }
234 |
235 | private void openDefaultNotebook(){
236 | if (currentKernel != null) {
237 | Msg.info(this, "There is already a kernel running");
238 | return;
239 | }
240 | var programValue = programOptions.getString(OPTION_LAST_URI, "");
241 | var toolValue = toolOptions.getString(OPTION_LAST_URI, "");
242 | var value = "";
243 | if (programValue.equals("") && toolValue.equals("")){
244 | var msg = String.format("The URI option was not set, please go to\n" +
245 | "'Edit'-> 'Options for %s' -> %s\n" +
246 | "and set the option to the full URL your default browser should navigate to", currentProgram.getName(), PLUGIN_NAME);
247 | Msg.showError(this, null,"No URI set in options", msg);
248 | return;
249 | }
250 | else if (programValue.equals("")){
251 | Msg.info(this, "No program specific notebook configured, but default option is available");
252 | value = toolValue;
253 | }
254 | else {
255 | value = programValue;
256 | }
257 | try {
258 | var uri = new URI(value);
259 | if (uri.getScheme().equals("http")) {
260 | currentKernel = new NotebookThread(cellContext, tool);
261 | runManager.runNow(currentKernel, "Notebook");
262 | openURI(uri);
263 | }
264 | else {
265 | Msg.showError(this, null, "Invalid URI", "Scheme of the URI option isn't http, this seems wrong");
266 | }
267 | } catch (URISyntaxException e) {
268 | Msg.showError(this, null, "Last URI Option is invalid",
269 | "Last URI in options was not a valid URI and parsing it threw an exception", e);
270 | }
271 |
272 |
273 | }
274 | @Override
275 | protected void programActivated(Program activatedProgram) {
276 | Msg.info(this, "Program activated");
277 | var state = new GhidraState(tool, tool.getProject(),
278 | currentProgram, currentLocation, currentSelection, currentHighlight);
279 | cellContext.set(state, TaskMonitor.DUMMY, null);
280 |
281 | programOptions = activatedProgram.getOptions(PLUGIN_NAME);
282 | programOptions.registerOption(OPTION_LAST_URI, OptionType.STRING_TYPE, "", null,
283 | "Saved URI");
284 | }
285 |
286 | protected void programClosed(Program program) {
287 | Msg.info(this, "Program closed");
288 | if (cellContext.getCurrentProgram() == program) {
289 | var state = new GhidraState(tool, tool.getProject(),
290 | null, null, null, null);
291 | cellContext.set(state, TaskMonitor.DUMMY, null);
292 | }
293 | }
294 |
295 | /**
296 | * Subclass should override this method if it is interested in
297 | * program location events.
298 | * @param loc location could be null
299 | */
300 | protected void locationChanged(ProgramLocation loc) {
301 | if (currentLocation != null) {
302 | cellContext.setCurrentLocation(currentLocation.getAddress());
303 | cellContext.currentContextLocation = loc;
304 | }
305 | }
306 |
307 | /**
308 | * Subclass should override this method if it is interested in
309 | * program selection events.
310 | * @param sel selection could be null
311 | */
312 | protected void selectionChanged(ProgramSelection sel) {
313 | cellContext.setCurrentSelection(currentSelection);
314 | }
315 |
316 | /**
317 | * Subclass should override this method if it is interested in
318 | * program highlight events.
319 | * @param hl highlight could be null
320 | */
321 | protected void highlightChanged(ProgramSelection hl) {
322 | if (hl != null) {
323 | cellContext.setCurrentHighlight(hl);
324 | }
325 | }
326 | }
327 |
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/KernelThread.java:
--------------------------------------------------------------------------------
1 | package GhidraJupyterKotlin;
2 |
3 | import ghidra.util.task.MonitoredRunnable;
4 |
5 | import java.io.File;
6 |
7 | public interface KernelThread extends MonitoredRunnable {
8 | File getConnectionFile();
9 | }
10 |
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/KotlinQtConsoleThread.java:
--------------------------------------------------------------------------------
1 | package GhidraJupyterKotlin;
2 |
3 | import ghidra.util.Msg;
4 | import ghidra.util.task.MonitoredRunnable;
5 | import ghidra.util.task.TaskMonitor;
6 | import org.jetbrains.kotlinx.jupyter.IkotlinKt;
7 | import org.jetbrains.kotlinx.jupyter.libraries.EmptyResolutionInfoProvider;
8 | import org.zeromq.ZMQException;
9 |
10 | import java.io.*;
11 | import java.util.*;
12 |
13 | public class KotlinQtConsoleThread implements KernelThread {
14 |
15 | private final CellContext context;
16 | private final File connectionFile;
17 |
18 | public KotlinQtConsoleThread(CellContext ctx, File connectionFile) {
19 | this.context = ctx;
20 | this.connectionFile = connectionFile;
21 | }
22 |
23 | @Override
24 | public void monitoredRun(TaskMonitor monitor) {
25 | Msg.info(this, connectionFile.toString());
26 | try {
27 | IkotlinKt.embedKernel(
28 | connectionFile,
29 | null,
30 | Collections.singletonList(context));
31 | } catch( ZMQException e){
32 | Msg.warn(this,"Kernel terminated, probably because of shutdown request?", e);
33 | }
34 | }
35 |
36 | @Override
37 | public File getConnectionFile() {
38 | return connectionFile;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/LineReader.java:
--------------------------------------------------------------------------------
1 | package GhidraJupyterKotlin;
2 |
3 | import ghidra.util.exception.CancelledException;
4 | import ghidra.util.task.TaskMonitor;
5 |
6 | import java.io.BufferedReader;
7 | import java.io.File;
8 | import java.io.FileNotFoundException;
9 | import java.io.FileReader;
10 | import java.io.IOException;
11 |
12 | class LineReader {
13 | BufferedReader reader;
14 |
15 | public LineReader(File file) throws FileNotFoundException {
16 | reader = new BufferedReader(new FileReader(file));
17 | }
18 |
19 | public String readLine(TaskMonitor monitor) throws IOException, CancelledException {
20 | while (!reader.ready()) {
21 | try {
22 | if (monitor.isCancelled()){
23 | throw new CancelledException();
24 | }
25 | Thread.sleep(1000);
26 | } catch (InterruptedException e) {
27 | e.printStackTrace();
28 | }
29 | }
30 | return reader.readLine();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/NotebookProxy.java:
--------------------------------------------------------------------------------
1 | package GhidraJupyterKotlin;
2 |
3 | import ghidra.util.exception.CancelledException;
4 | import ghidra.util.task.TaskMonitor;
5 |
6 | import java.io.File;
7 | import java.io.IOException;
8 | import java.nio.file.Files;
9 | import java.nio.file.Path;
10 | import java.nio.file.StandardOpenOption;
11 |
12 | public class NotebookProxy {
13 | File proxyFile;
14 | File pidFile;
15 |
16 | public NotebookProxy(Path proxyBase) {
17 | this.proxyFile = new File(proxyBase.toString() + ".path");
18 | this.pidFile = new File(proxyBase.toString() + ".pid");
19 | }
20 |
21 | public File waitForConnection(TaskMonitor monitor) throws IOException, CancelledException {
22 | // Ensure that the directory exists (create it if needed)
23 | var parent = proxyFile.getParentFile();
24 | if (!parent.exists()) {
25 | if (!parent.mkdirs()){
26 | throw new IOException("Failed to create directory: " + parent);
27 | }
28 | }
29 |
30 | // Write the PID to file so that the
31 | pidFile.delete();
32 | pidFile.createNewFile();
33 | pidFile.deleteOnExit();
34 |
35 | long pid = ProcessHandle.current().pid();
36 | Files.write(Path.of(pidFile.getPath()), Long.toString(pid).getBytes(), StandardOpenOption.APPEND);
37 |
38 | // Create a proxy file and wait for a line on it
39 | proxyFile.delete();
40 | proxyFile.createNewFile();
41 | proxyFile.deleteOnExit();
42 |
43 | var lineReader = new LineReader(proxyFile);
44 |
45 | return new File(lineReader.readLine(monitor));
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/NotebookThread.java:
--------------------------------------------------------------------------------
1 | package GhidraJupyterKotlin;
2 |
3 |
4 | import ghidra.framework.plugintool.PluginTool;
5 | import ghidra.util.Msg;
6 | import ghidra.util.exception.CancelledException;
7 | import ghidra.util.task.MonitoredRunnable;
8 | import ghidra.util.task.Task;
9 | import ghidra.util.task.TaskMonitor;
10 | import org.jetbrains.kotlinx.jupyter.IkotlinKt;
11 | import org.jetbrains.kotlinx.jupyter.libraries.EmptyResolutionInfoProvider;
12 |
13 | import java.io.File;
14 | import java.io.IOException;
15 | import java.nio.file.Path;
16 | import java.util.Collections;
17 | import java.util.Optional;
18 |
19 | public class NotebookThread implements KernelThread {
20 | /**
21 | * Steps:
22 | * 1. Create proxy file
23 | * 2. Wait on proxy file
24 | * 3. Read connection file path
25 | * 4. Embed kernel
26 | */
27 |
28 | private final CellContext context;
29 | private final PluginTool tool;
30 |
31 |
32 | @Override
33 | public File getConnectionFile() {
34 | return connectionFile;
35 | }
36 |
37 | private File connectionFile = null;
38 |
39 | public NotebookThread(CellContext ctx, PluginTool tool) {
40 | this.context = ctx;
41 | this.tool = tool;
42 | }
43 |
44 | private class WaitTask extends Task {
45 | public WaitTask() {
46 | super("Waiting for Jupyter Notebook connection",
47 | true,
48 | false,
49 | true,
50 | true);
51 | }
52 |
53 | @Override
54 | public void run(TaskMonitor taskMonitor) throws CancelledException {
55 | var proxyPath = Path.of(Optional.ofNullable(System.getenv("GHIDRA_JUPYTER_PROXY"))
56 | .orElse(
57 | Path.of(System.getProperty("user.home"))
58 | .resolve(".ghidra")
59 | .resolve("notebook_proxy")
60 | .toString()
61 | ));
62 | try {
63 | connectionFile = new NotebookProxy(proxyPath).waitForConnection(taskMonitor);
64 | } catch (IOException e) {
65 | e.printStackTrace();
66 | }
67 | }
68 | }
69 |
70 | @Override
71 | public void monitoredRun(TaskMonitor monitor) {
72 | if (connectionFile == null) {
73 | tool.execute(new WaitTask());
74 | }
75 |
76 | Msg.info(this, connectionFile.toString());
77 | IkotlinKt.embedKernel(
78 | connectionFile,
79 | null,
80 | Collections.singletonList(context));
81 | }
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/ShutDownKernelAction.kt:
--------------------------------------------------------------------------------
1 | //package GhidraJupyterKotlin
2 | //
3 | //import docking.ActionContext
4 | //import docking.action.DockingAction
5 | //import org.jetbrains.kotlinx.jupyter.startup.KernelJupyterParams
6 | ////import org.jetbrains.kotlinx.jupyter.*
7 | ////import org.jetbrains.kotlinx.jupyter.messaging.*
8 | //import org.zeromq.ZMQ
9 | //import java.util.*
10 | //
11 | //
12 | //class ShutDownKernelAction(var parentPlugin: JupyterKotlinPlugin): DockingAction("Interrupt Kernel", parentPlugin.name) {
13 | // override fun actionPerformed(context: ActionContext?) {
14 | // // This creates a ZMQ socket, to send a shutdown request to the running kernel
15 | // // This is basically a duplicate of InterruptKernelAction and could be merged in the future
16 | // val config: KernelJupyterParams = KernelJupyterParams.fromFile(parentPlugin.connectionFile)
17 | //
18 | // val ctx = ZMQ.context(1)
19 | // val control = ctx.socket(JupyterSockets.CONTROL.zmqClientType)
20 | // control.connect("${config.transport}://*:${config.ports[JupyterSockets.CONTROL.ordinal]}")
21 | // if (config.sigScheme == "hmac-sha256"){
22 | // val hmac = HMAC("HmacSHA256", config.key!!)
23 | // control.sendMessage(
24 | // Message(id = listOf(byteArrayOf(1)),
25 | // MessageData(
26 | // header = makeHeader(
27 | // MessageType.SHUTDOWN_REQUEST,
28 | // sessionId = UUID.randomUUID().toString()),
29 | // content = ShutdownRequest(false))),
30 | // hmac)
31 | // parentPlugin.clearKernel()
32 | // }
33 | //
34 | //
35 | // }
36 | //}
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/extensions/address/AddressExtensions.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("unused")
2 | //They are imported in some script or the kernel itself by the user
3 | // use them by adding the following import to script
4 | // import GhidraJupyterKotlin.extensions.address.*
5 | package GhidraJupyterKotlin.extensions.address
6 |
7 | import ghidra.program.model.address.Address
8 | import ghidra.program.model.address.AddressRange
9 | import ghidra.program.model.address.AddressRangeImpl
10 |
11 |
12 |
13 | /**
14 | * `currentAddress+10` returns a new Address
15 | */
16 | operator fun Address.plus(rhs: Long): Address {
17 | return this.addNoWrap(rhs)
18 | }
19 | operator fun Address.plus(rhs: Int): Address {
20 | return this.addNoWrap(rhs.toLong())
21 | }
22 |
23 | /**
24 | * `currentAddress-10` returns a new Address
25 | */
26 | operator fun Address.minus(rhs: Long): Address {
27 | return this.subtractNoWrap(rhs)
28 | }
29 | operator fun Address.minus(rhs: Int): Address {
30 | return this.subtractNoWrap(rhs.toLong())
31 | }
32 |
33 | operator fun Address.minus(rhs: Address): Long {
34 | return this.subtract(rhs)
35 | }
36 |
37 | /**
38 | * `currentAddress..otherAddress` gives an AddressRange with currentAddress as start, and otherAddress as end
39 | */
40 | operator fun Address.rangeTo(rhs: Address): AddressRange {
41 | return AddressRangeImpl(this, rhs)
42 | }
43 |
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/extensions/data/DataExtensions.kt:
--------------------------------------------------------------------------------
1 | package GhidraJupyterKotlin.extensions.data
2 |
3 | import ghidra.program.model.data.Structure
4 | import ghidra.program.model.listing.Data
5 |
6 |
7 |
8 | // For a Data object that supports component (like arrays or structs) you can use
9 | // `data[i]` instead of `data.getComponent(i)`
10 | operator fun Data.get(i: Int): Data? {
11 | return this.getComponent(i)
12 | }
13 |
14 | // For a Data object that represents a struct you can use
15 | // `data[fieldName]`
16 |
17 | operator fun Data.get(name: String): Data? {
18 | if (this.dataType is Structure){
19 | val s = (this.dataType as Structure)
20 | val idx = s.components.firstOrNull { it.fieldName == name }?.ordinal
21 | return idx?.let(this::getComponent)
22 | }
23 | return null
24 | }
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/extensions/misc/MiscExtensions.kt:
--------------------------------------------------------------------------------
1 | package GhidraJupyterKotlin.extensions.misc
2 |
3 | import ghidra.framework.model.DomainObject
4 | import ghidra.program.model.listing.FunctionIterator
5 | import ghidra.program.model.listing.FunctionManager
6 | import ghidra.program.model.listing.Program
7 |
8 | val FunctionManager.functions: FunctionIterator
9 | get() = this.getFunctions(true)
10 |
11 |
12 | val Program.functions: FunctionIterator
13 | get() = this.functionManager.getFunctions(true)
14 |
15 |
16 | fun DomainObject.runTransaction(description: String, transaction: () -> Unit) {
17 | val transactionID: Int = this.startTransaction(description)
18 | try {
19 | transaction()
20 | this.endTransaction(transactionID, true)
21 | }
22 | catch (e: Throwable) {
23 | this.endTransaction(transactionID, false)
24 | throw e
25 | }
26 | }
27 |
28 | fun DomainObject.runTransaction(transaction: () -> Unit){
29 | val transactionID: Int = this.startTransaction("Kotlin Lambda Transaction")
30 | try {
31 | transaction()
32 | this.endTransaction(transactionID, true)
33 | }
34 | catch (e: Throwable) {
35 | this.endTransaction(transactionID, false)
36 | throw e
37 | }
38 | }
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/src/main/java/ghidra/app/script/KotlinCompilerMessageCollector.kt:
--------------------------------------------------------------------------------
1 | package ghidra.app.script
2 |
3 | import generic.jar.ResourceFile
4 | import ghidra.util.Msg
5 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
6 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation
7 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector
8 |
9 | class KotlinCompilerMessageCollector(private val sourceFile: ResourceFile) : MessageCollector {
10 | override fun clear() {
11 | return
12 | }
13 |
14 | override fun hasErrors(): Boolean {
15 | return false
16 | }
17 |
18 | override fun toString(): String {
19 | return "MessageCollector for compilation of $sourceFile"
20 | }
21 | override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageSourceLocation?) {
22 | when (severity){
23 | CompilerMessageSeverity.EXCEPTION -> Msg.error(this, message)
24 | CompilerMessageSeverity.ERROR -> Msg.error(this, message)
25 | CompilerMessageSeverity.STRONG_WARNING -> Msg.warn(this, message)
26 | CompilerMessageSeverity.WARNING -> Msg.warn(this, message)
27 | CompilerMessageSeverity.INFO -> Msg.info(this, message)
28 | CompilerMessageSeverity.LOGGING -> Msg.debug(this, message)
29 | CompilerMessageSeverity.OUTPUT -> Msg.out(message)
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/src/main/java/ghidra/app/script/KotlinScriptProvider.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package ghidra.app.script;
17 |
18 | import generic.io.NullPrintWriter;
19 | import generic.jar.ResourceFile;
20 | import ghidra.app.util.headless.HeadlessScript;
21 | import ghidra.util.Msg;
22 | import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys;
23 | import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments;
24 | import org.jetbrains.kotlin.cli.common.config.ContentRootsKt;
25 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity;
26 | import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler;
27 | import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles;
28 | import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment;
29 | import org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler;
30 | import org.jetbrains.kotlin.cli.jvm.config.JvmContentRootsKt;
31 | import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer;
32 | import org.jetbrains.kotlin.config.CommonConfigurationKeys;
33 | import org.jetbrains.kotlin.config.CompilerConfiguration;
34 | import org.jetbrains.kotlin.config.JVMConfigurationKeys;
35 | import org.jetbrains.kotlin.utils.PathUtil;
36 |
37 | import java.io.File;
38 | import java.io.FileWriter;
39 | import java.io.IOException;
40 | import java.io.PrintWriter;
41 | import java.lang.reflect.InvocationTargetException;
42 | import java.net.MalformedURLException;
43 | import java.net.URL;
44 | import java.net.URLClassLoader;
45 | import java.util.ArrayList;
46 | import java.util.Arrays;
47 | import java.util.List;
48 | import java.util.stream.Collectors;
49 |
50 |
51 | @SuppressWarnings("unused")
52 | // This is an ExtensionPoint, so the class loader automatically searches for classes ending in "ScriptProvider"
53 | // and sets them up
54 | public class KotlinScriptProvider extends GhidraScriptProvider {
55 |
56 | @Override
57 | public String getDescription() {
58 | return "Kotlin";
59 | }
60 |
61 | @Override
62 | public String getExtension() {
63 | return ".kt";
64 | }
65 |
66 | @Override
67 | public boolean deleteScript(ResourceFile scriptSource) {
68 | // Assuming script is in default java package, so using script's base name as class name.
69 | File clazzFile = getClassFile(scriptSource, GhidraScriptUtil.getBaseName(scriptSource));
70 | //noinspection ResultOfMethodCallIgnored
71 | clazzFile.delete();
72 | return super.deleteScript(scriptSource);
73 | }
74 |
75 | @Override
76 | public GhidraScript getScriptInstance(ResourceFile sourceFile, PrintWriter writer)
77 | throws GhidraScriptLoadException {
78 |
79 | if (writer == null) {
80 | writer = new NullPrintWriter();
81 | }
82 |
83 | // Assuming script is in default java package, so using script's base name as class name.
84 | File clazzFile = getClassFile(sourceFile, GhidraScriptUtil.getBaseName(sourceFile));
85 | try {
86 | compile(sourceFile, writer); // may throw an exception
87 | } catch (ClassNotFoundException e) {
88 | throw new GhidraScriptLoadException("The class could not be found. " +
89 | "It must be the public class of the .java file: " + e.getMessage(), e);
90 | }
91 |
92 |
93 | Class> clazz;
94 | try {
95 | clazz = getScriptClass(sourceFile);
96 | }
97 | catch (GhidraScriptUnsupportedClassVersionError e) {
98 | // Unusual Code Alert!: This implies the script was compiled in a newer
99 | // version of Java. So, just delete the class file and try again.
100 | ResourceFile classFile = e.getClassFile();
101 | classFile.delete();
102 | return getScriptInstance(sourceFile, writer);
103 | }
104 |
105 | Object object = null;
106 | try {
107 | // If clazz is null for some reason crashing with it might make it more obvious where the issue lies
108 | //noinspection ConstantConditions
109 | object = clazz.getDeclaredConstructor().newInstance();
110 | } catch (InvocationTargetException | NoSuchMethodException e) {
111 | throw new GhidraScriptLoadException(e);
112 | } catch (InstantiationException | IllegalAccessException e) {
113 | throw new GhidraScriptLoadException(e);
114 | }
115 | if (object instanceof GhidraScript) {
116 | GhidraScript script = (GhidraScript) object;
117 | script.setSourceFile(sourceFile);
118 | return script;
119 | }
120 |
121 | String message = "Not a valid Ghidra script: " + sourceFile.getName();
122 | writer.println(message);
123 | Msg.error(this, message); // the writer may not be the same as Msg, so log it too
124 | return null; // class is not a script
125 | }
126 |
127 |
128 | /**
129 | * Gets the class file corresponding to the given source file and class name.
130 | * If the class is in a package, the class name should include the full
131 | * package name.
132 | *
133 | * @param sourceFile The class's source file.
134 | * @param className The class's name (including package if applicable).
135 | * @return The class file corresponding to the given source file and class name.
136 | */
137 | protected File getClassFile(ResourceFile sourceFile, String className) {
138 | ResourceFile resourceFile =
139 | getClassFileByResourceFile(sourceFile, className);
140 |
141 | return resourceFile.getFile(false);
142 | }
143 |
144 | static ResourceFile getClassFileByResourceFile(ResourceFile sourceFile, String rawName) {
145 | String javaAbsolutePath = sourceFile.getAbsolutePath();
146 | String classAbsolutePath = javaAbsolutePath.replace(".java", ".class");
147 |
148 | return new ResourceFile(classAbsolutePath);
149 | }
150 |
151 | protected boolean needsCompile(ResourceFile sourceFile, File classFile) {
152 |
153 | // Need to compile if there is no class file.
154 | if (!classFile.exists()) {
155 | return true;
156 | }
157 |
158 | // Need to compile if the script's source file is newer than its corresponding class file.
159 | if (sourceFile.lastModified() > classFile.lastModified()) {
160 | return true;
161 | }
162 |
163 | // Need to compile if parent classes are not up to date.
164 | return !areAllParentClassesUpToDate(sourceFile);
165 | }
166 |
167 |
168 | private boolean areAllParentClassesUpToDate(ResourceFile sourceFile) {
169 |
170 | List> parentClasses = getParentClasses(sourceFile);
171 | if (parentClasses == null) {
172 | // some class is missing!
173 | return false;
174 | }
175 |
176 | if (parentClasses.isEmpty()) {
177 | // nothing to do--no parent class to re-compile
178 | return true;
179 | }
180 |
181 | // check each parent for modification
182 | for (Class> clazz : parentClasses) {
183 | ResourceFile parentFile = getSourceFile(clazz);
184 | if (parentFile == null) {
185 | continue; // not sure if this can happen (inner-class, maybe?)
186 | }
187 |
188 | // Parent class might have a non-default java package, so use class's full name.
189 | File clazzFile = getClassFile(parentFile, clazz.getName());
190 |
191 | if (parentFile.lastModified() > clazzFile.lastModified()) {
192 | return false;
193 | }
194 | }
195 |
196 | return true;
197 | }
198 |
199 | protected void compile(ResourceFile sourceFile, final PrintWriter writer)
200 | throws ClassNotFoundException {
201 | if (!doEmbeddedCompile(sourceFile, writer)) {
202 | writer.flush(); // force any error messages out
203 | throw new ClassNotFoundException("Unable to compile class: " + sourceFile.getName());
204 | }
205 | writer.println("Successfully compiled: " + sourceFile.getName());
206 | }
207 |
208 | private K2JVMCompilerArguments getCompilerArgs(K2JVMCompiler compiler){
209 | var arguments = compiler.createArguments();
210 | var cp = getClassPath();
211 |
212 | arguments.setClasspath(cp);
213 |
214 | return arguments;
215 | }
216 | private boolean doEmbeddedCompile(ResourceFile sourceFile, final PrintWriter writer) {
217 | Msg.info(this, "Compiling sourceFile: " + sourceFile.getAbsolutePath());
218 |
219 | if (System.getProperty("os.name").startsWith("Windows")) {
220 | System.getProperties().setProperty("idea.io.use.nio2", java.lang.Boolean.TRUE.toString());
221 | }
222 |
223 | var rootDisposable = Disposer.newDisposable();
224 | var compiler = new K2JVMCompiler();
225 | var args = getCompilerArgs(compiler);
226 | var compilerConfiguration = new CompilerConfiguration();
227 | // TODO: What is a good module name here?
228 | compilerConfiguration.put(CommonConfigurationKeys.MODULE_NAME, "SOME_MODULE_NAME");
229 | var collector = new KotlinCompilerMessageCollector(sourceFile);
230 | compilerConfiguration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, collector);
231 | JvmContentRootsKt.addJvmClasspathRoots(compilerConfiguration, PathUtil.getJdkClassesRootsFromCurrentJre());
232 | JvmContentRootsKt.addJvmClasspathRoots(compilerConfiguration, getClassPathAsFiles());
233 | ContentRootsKt.addKotlinSourceRoot(compilerConfiguration, sourceFile.toString());
234 | compilerConfiguration.put(JVMConfigurationKeys.OUTPUT_DIRECTORY, outputDir(sourceFile).getFile(false));
235 |
236 | // This shouldn't be needed and is a workaround for a bug in the Kotlin compiler
237 | // https://youtrack.jetbrains.com/issue/KT-20167/JDK-9-unresolved-supertypes-Object-when-working-with-Kotlin-Scripting-API
238 | compilerConfiguration.put(JVMConfigurationKeys.JDK_HOME, new File(System.getProperty("java.home")));
239 |
240 | var disposable = Disposer.newDisposable();
241 |
242 | KotlinCoreEnvironment env = KotlinCoreEnvironment.createForProduction(
243 | disposable, compilerConfiguration, EnvironmentConfigFiles.JVM_CONFIG_FILES);
244 |
245 | return KotlinToJVMBytecodeCompiler.INSTANCE.compileBunchOfSources(env);
246 | }
247 |
248 | private List getClassPathAsFiles(){
249 | return Arrays.stream(System.getProperty("java.class.path").split(File.pathSeparator))
250 | .map(File::new)
251 | // There might be files like "ExtensionPoint.manifest" as a classpath entry
252 | // the Kotlin compiler tries to open them as .jars (ZIP) and fails, so filter them out
253 | .filter(it -> it.getName().endsWith(".jar") || it.isDirectory())
254 | .collect(Collectors.toList());
255 | }
256 |
257 | private ResourceFile outputDir(ResourceFile sourceFile) {
258 | return sourceFile.getParentFile();
259 | }
260 |
261 | private List> getParentClasses(ResourceFile scriptSourceFile) {
262 |
263 | Class> scriptClass = getScriptClass(scriptSourceFile);
264 | if (scriptClass == null) {
265 | return null; // special signal that there was a problem
266 | }
267 |
268 | List> parentClasses = new ArrayList<>();
269 | Class> superClass = scriptClass.getSuperclass();
270 | while (superClass != null) {
271 | if (superClass.equals(GhidraScript.class)) {
272 | break; // not interested in the built-in classes
273 | } else if (superClass.equals(HeadlessScript.class)) {
274 | break; // not interested in the built-in classes
275 | }
276 | parentClasses.add(superClass);
277 | superClass = superClass.getSuperclass();
278 | }
279 | return parentClasses;
280 | }
281 |
282 | private Class> getScriptClass(ResourceFile scriptSourceFile) {
283 | String clazzName = GhidraScriptUtil.getBaseName(scriptSourceFile);
284 | try {
285 | URL classURL = outputDir(scriptSourceFile).getFile(false).toURI().toURL();
286 | ClassLoader cl = new URLClassLoader(new URL[] {classURL});
287 | return cl.loadClass(clazzName);
288 | }
289 | catch (NoClassDefFoundError | ClassNotFoundException e) {
290 | Msg.error(this, "Unable to find class file for script file: " + scriptSourceFile, e);
291 |
292 | }
293 | catch (MalformedURLException e) {
294 | Msg.error(this, "Malformed URL exception:", e);
295 | }
296 | return null;
297 | }
298 |
299 | private ResourceFile getSourceFile(Class> c) {
300 | // check all script paths for a dir named
301 | String classname = c.getName();
302 | String filename = classname.replace('.', '/') + ".kt";
303 |
304 | List scriptDirs = GhidraScriptUtil.getScriptSourceDirectories();
305 | for (ResourceFile dir : scriptDirs) {
306 | ResourceFile possibleFile = new ResourceFile(dir, filename);
307 | if (possibleFile.exists()) {
308 | return possibleFile;
309 | }
310 | }
311 |
312 | return null;
313 | }
314 |
315 | private String getClassPath() {
316 | return System.getProperty("java.class.path");
317 | }
318 |
319 | @Override
320 | public void createNewScript(ResourceFile newScript, String category) throws IOException {
321 | String scriptName = newScript.getName();
322 | String className = scriptName;
323 | int dotPos = scriptName.lastIndexOf('.');
324 | if (dotPos >= 0) {
325 | className = scriptName.substring(0, dotPos);
326 | }
327 | PrintWriter writer = new PrintWriter(new FileWriter(newScript.getFile(false)));
328 |
329 | writeHeader(writer, category);
330 |
331 | writer.println("import ghidra.app.script.GhidraScript");
332 |
333 | for (Package pkg : Package.getPackages()) {
334 | if (pkg.getName().startsWith("ghidra.program.model.")) {
335 | writer.println("import " + pkg.getName() + ".*");
336 | }
337 | }
338 |
339 | writer.println("");
340 |
341 | writer.println("class " + className + " : GhidraScript() {");
342 |
343 | writer.println(" @Throws(Exception::class)");
344 | writer.println(" override fun run() {");
345 |
346 | writeBody(writer);
347 |
348 | writer.println(" }");
349 | writer.println("");
350 | writer.println("}");
351 | writer.println("");
352 | writer.close();
353 | }
354 |
355 | @Override
356 | public String getCommentCharacter() {
357 | return "//";
358 | }
359 | }
360 |
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/src/main/resources/images/README.txt:
--------------------------------------------------------------------------------
1 | The "src/resources/images" directory is intended to hold all image/icon files used by
2 | this module.
3 |
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/src/main/resources/images/notebook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/GhidraJupyterKotlin/src/main/resources/images/notebook.png
--------------------------------------------------------------------------------
/GhidraJupyterKotlin/src/main/resources/images/qtconsole.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/GhidraJupyterKotlin/src/main/resources/images/qtconsole.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 GhidraJupyter
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ghidra-Jupyter
2 |
3 | ## Automatic Installation
4 |
5 | 1. Use pip to install the kernel and the management utility
6 |
7 | ```bash
8 | pip install ghidra-jupyter
9 | ```
10 |
11 | 2. Use the management utility to install the extension.
12 | Make sure `$GHIDRA_INSTALL_DIR` is defined,
13 | as it points the utility to the right path.
14 |
15 | ```bash
16 | ghidra-jupyter install-extension
17 | ```
18 |
19 | 3. If you have multiple installations of Ghidra,
20 | you can point the installer to the right one.
21 |
22 | ```bash
23 | ghidra-jupyter install-extension --ghidra
24 | ```
25 |
26 | ## Manual Installation
27 |
28 | 1. Use pip to install the kernel and the management utility
29 |
30 | ```bash
31 | pip install ghidra-jupyter
32 | ```
33 |
34 | 2. Download the [latest release](https://github.com/GhidraJupyter/ghidra-jupyter-kotlin/releases/latest) zip from our releases page
35 | 3. Place the zip under `$GHIDRA_INSTALL_DIR/Ghidra/Extensions/` or select it via the GUI dialog to install extensions
36 |
37 | ## Usage
38 |
39 | After installation, you should be prompted about a new plugin when opening the CodeBrowser. Confirm the installation and activate it via "File -> Configure..." and ticking the checkbox for the "Miscellaneous" Group.
40 |
41 | Directly after you'll see 2 new buttons and a new menu inside Ghidra.
42 |
43 | 
44 |
45 | 
46 |
47 |
48 | The third action is only available in the menu and provides a shortcut
49 | to open an already running `juptyter-notebook` server or to start a new one.
50 |
51 |
52 | ### Kotlin QtConsole
53 |
54 | This feature requires the Jupyter QT Console to be installed and `jupyter-qtconsole` to be available in your `PATH`. This is a separate package on PyPI and in most distros, so you typically need to explicitly install it.
55 |
56 | Click the ![QtConsole] button to open a QtConsole.
57 |
58 | Once you click, a Jupyter Kernel will be initialized in the current Ghidra program
59 | and the Jupyter QtConsole will launch. If there is already a notebook kernel running,
60 | the console will use the same kernel (i.e. share the variables, functions, etc.)
61 |
62 | 
63 |
64 | #### Caveats
65 |
66 | If you want to interrupt the code you executed, the menu action "Interrupt Current Kernel" or "Ctrl+C" will NOT work. It will simply print `Cannot interrupt a kernel I did not start.`
67 |
68 | This is a limitation of the Jupyter QT console. To work around this issue, the plugin provides an action `Interrupt Execution` in the `Jupyter` submenu. This will interrupt the currently executed cell:
69 |
70 | 
71 |
72 | ### Jupyter Terminal Console
73 |
74 | If you want to use the terminal based `jupyter-console` instead,
75 | open a CodeBrowser instance and in the top bar navigate to
76 |
77 | `Edit -> Tool Options ... -> JupyterKotlinPlugin`
78 |
79 | 
80 |
81 | Here you can set the `CONSOLE_CMD` to whatever command opens your preferred terminal and runs
82 | `jupyter-console --existing` in it. The connection file will be appended to it, so the full command called later will be
83 | something like:
84 |
85 | ```sh
86 | kitty -- jupyter-console --existing /home/user/.local/share/jupyter/runtime/kernel-3af276c1-ac24-4368-bbb4-94cdf082fa7a.json
87 | ```
88 |
89 | ### Jupyter Notebook
90 |
91 | 1. Start Jupyter Notebook or Jupyter Lab
92 |
93 | ```bash
94 | jupyter notebook
95 | ```
96 |
97 | or by using the menu action.
98 |
99 | 2. Click the ![Notebook] button in Ghidra to accept a notebook connection
100 |
101 | The following popup will show, indicating that Ghidra is actively waiting
102 |
103 | 
104 |
105 | 3. In the Jupyter Notebook home page, create a Ghidra(Kotlin) notebook
106 |
107 | 
108 |
109 | Once you do, the notebook will connect to your waiting Ghidra instance.
110 |
111 | 
112 |
113 | [QtConsole]:resources/readme/qtconsole.png
114 | [Notebook]:resources/readme/notebook.png
115 |
116 | ## Demo Snippets
117 |
118 | These snippets can be pasted directly in the QT console or a notebook cell.
119 |
120 | ### Kotlin Extensions
121 |
122 | [Extensions](https://kotlinlang.org/docs/extensions.html#extensions-are-resolved-statically) are a Kotlin feature which allows extending existing classes with new methods, properties or operators. This allows various convenience features, especially combined with other Kotlin Features like operator overloading and easily providing lambdas. They need to be explicitly imported in your script/kernel before using them:
123 |
124 | ```kotlin
125 | // Import all extensions in the GhidraJupyterKotlin.extensions.address package
126 | import GhidraJupyterKotlin.extensions.address.*
127 | ```
128 |
129 | If you end up writing any kind of extension method/property/operator we would be happy to receive a PR.
130 | Not all extension provided are documented in the README.md, check the [extensions folder](./GhidraJupyterKotlin/src/main/java/GhidraJupyterKotlin/extensions) for all of them. Nearly all of them are fairly simple (a few lines at most) so they can also serve as good examples how to write your own.
131 |
132 | #### Explicit Database Transactions
133 |
134 | Unlike the Jython REPL, the Kotlin Kernel does NOT wrap each cell in an implicit Database transaction. Any attempt to modify the Database will result in `NoTransactionException: Transaction has not been started`.
135 |
136 | Instead, there is an extension method on the `UndoableDomainObject` interface, that makes Database transactions explicit with minimal syntactic overhead:
137 |
138 | ```kotlin
139 | import GhidraJupyterKotlin.extensions.misc.*
140 |
141 | currentProgram.runTransaction {
142 | /* your code modifying the DB */
143 | currentProgram.name = "NewName"
144 | }
145 | ```
146 |
147 | If the code throws any kind of Exception the transaction will be aborted and the changes will be discarded.
148 |
149 | This method can also be called with a transaction description:
150 | ```kotlin
151 | currentProgram.runTransaction("Transaction Description") {
152 | currentProgram.name = "NewName"
153 | }
154 | ```
155 |
156 | For comparison, the regular Ghidra API for transactions:
157 | ```kotlin
158 | val transactionID = currentProgram.startTransaction("Transaction Description")
159 | /* your code modifying the DB */
160 | currentProgram.name = "NewName"
161 | currentProgram.endTransaction(transactionID, true) // true means the changes should be committed to the DB
162 | ```
163 |
164 |
165 |
166 | #### Address Arithmetic with Operators
167 |
168 | Unlike Java, Kotlin supports [operator overloading](https://kotlinlang.org/docs/operator-overloading.html). This can be used to make calculations involving addresses more comfortable:
169 |
170 | ```kotlin
171 | import GhidraJupyterKotlin.extensions.address.*
172 | import ghidra.program.model.address.Address
173 | import ghidra.program.model.address.AddressRange
174 |
175 | val x: Address = currentAddress + 0x10 // Address + Offset (Int or Long)
176 | val y: Address = currentAddress - 0x10 // Address - Offset (Int or Long)
177 | val z: Address = x - y // Difference between Addresses
178 |
179 | val range: AddressRange = y..x // The range of addresses between currentAddress-0x10 and currentAddress+0x10
180 |
181 | ```
182 |
183 | ### Defining your own Imports and Helpers for Quick Access
184 |
185 | You can load a JSON file e.g. `decompiler.json`
186 |
187 | ```json
188 | {
189 | "link": "https://github.com/GhidraJupyter/ghidra-jupyter-kotlin",
190 | "description": "Shortcut to load features in Kotlin Kernel",
191 | "imports": [
192 | "GhidraJupyterKotlin.extensions.address.*",
193 | "GhidraJupyterKotlin.extensions.data.*",
194 | "GhidraJupyterKotlin.extensions.misc.*",
195 | "ghidra.app.decompiler.*"
196 | ],
197 | "init" : [
198 | "val ClangNode.parent: ClangTokenGroup; get() = this.Parent() as ClangTokenGroup",
199 | "val ClangNode.children: List; get() = (0..this.numChildren()-1).map(this::Child)",
200 | "val currentToken: ClangToken?; get() = (currentLocation as? DecompilerLocation)?.token"
201 | ]
202 | }
203 | ```
204 |
205 | and then activate it:
206 | ```
207 | %use /PATH/TO/FILE/decompiler
208 | ```
209 |
210 | If the file is placed under `$HOME/.jupyter_kotlin/libraries`, you only need to use the name, e.g. `%use decompiler`
211 |
212 | When activated, the `imports` will be made available just like with a manual import
213 | and each line in `init` will be run once. This means that the extension methods defined in the imports are available too.
214 | You can also import other packages to easily access classes, e.g. for `(currentLocation as DecompilerLocation).token`
215 |
216 | The `init` block in this example defines three [Extension Properties](https://kotlinlang.org/docs/extensions.html#extension-properties).
217 | 1. Work around the annoying name of the `ClangNode.Parent()` function and
218 | the quirk that this should always be a `ClangTokenGroup`, but isn't typed as such in Ghidra itself.
219 | 2. A function that collects all the children of a node into a list (which then works with `.map` etc.)
220 | 3. Add a value/property to directly access the currently selected token.
221 |
222 | Combined this means you can now open a console via the GUI, run `%use decompiler`, click on a token in the decompiler,
223 | and run
224 | ```
225 | In [1]: %use decompilerKotlin
226 | In [2]: currentToken.parent.children
227 | Out[2]: [pcVar2, , =, , "startUpdatingLocation"]
228 | ```
229 |
230 | For a full list of possibilities check the [official documentation](https://github.com/Kotlin/kotlin-jupyter/blob/master/docs/libraries.md#creating-library-descriptor)
231 |
232 |
233 | ## Building the Ghidra Plugin
234 |
235 | (requires at least Ghidra 10.1 to be this easy, earlier versions require copying libraries around)
236 | 1. Run `gradlew buildExtension -PGHIDRA_INSTALL_DIR=/path/to/ghidra_10.1_PUBLIC` and gradle should take care of
237 | pulling the dependencies, copying them to the `./lib` folder, and building the extension zip to
238 | `./GhidraJupyterKotlin/dist/*zip`
239 | 2. Install the plugin using the ghidra-jupyter installer
240 | ```bash
241 | ghidra-jupyter install-extension --extension-path GhidraJupyterKotlin/dist/
242 | ```
243 |
244 | ### Development (Ghidra Extension itself, Kotlin GhidraScripts and Kotlin Extension Methods)
245 |
246 | Developing this extension is only tested and supported with IntelliJ, which isn't officially supported as an IDE for
247 | Ghidra Extensions.
248 |
249 | First make sure that your `GHIDRA_INSTALL_DIR` variable is set in some way that gradle recognizes,
250 | e.g. by adding the line `GHIDRA_INSTALL_DIR=/path/to/ghidra_10.1_PUBLIC/`
251 | to the global gradle config in `$HOME/.gradle/gradle.properties`
252 |
253 | Now it should be enough to import the `settings.gradle` file via the
254 | IntelliJ IDEA "Project From Existing Sources" menu.
255 | IntelliJ will import the gradle project, fetch dependencies and
256 | afterwards you can click the `Build` button (Hammer in the top right menu) and it should compile successfully.
257 | If there is an error during importing or building please open an issue here on GitHub.
258 |
259 | The repo also includes a run configuration under `.idea/runConfigurations/Ghidra_GUI.xml` that IntelliJ should
260 | automatically pick up after the project was created, which will add a run configuration called `Ghidra GUI`.
261 | This run configuration will launch Ghidra with the current
262 | development code of the plugin. It will look fairly ugly because none of the VM options that influence that are included.
263 | To remedy this, generate the VM options for your system and set them as the VM options in the run configuration.
264 |
265 | ```sh
266 | cd $GHIDRA_INSTALL_DIR
267 | java -cp ./support/LaunchSupport.jar LaunchSupport ./support/.. -vmargs
268 | ```
269 |
270 |
271 | ## Licenses
272 |
273 | This project is released under the MIT license.
274 |
275 | The project uses components that are released under different licenses:
276 |
277 | - [kotlin-jupyter](https://github.com/Kotlin/kotlin-jupyter) is released under the Apache-2.0 License
278 | - The Kotlin runtime and libraries are released under the Apache-2.0 License
279 |
--------------------------------------------------------------------------------
/ghidra_jupyter/kernel/kernel.css:
--------------------------------------------------------------------------------
1 | /*
2 | This file includes code of JupyterLab project (https://github.com/jupyterlab/jupyterlab)
3 | which is licensed under the terms of the Modified BSD License
4 | (also known as New or Revised or 3-Clause BSD), as follows:
5 |
6 | - Copyright (c) 2015 Project Jupyter Contributors
7 |
8 | All rights reserved.
9 |
10 | Full license text is available in additional-licenses/LICENSE_BSD_3 file
11 | */
12 |
13 | :root {
14 | /* Elevation
15 | *
16 | * We style box-shadows using Material Design's idea of elevation. These particular numbers are taken from here:
17 | *
18 | * https://github.com/material-components/material-components-web
19 | * https://material-components-web.appspot.com/elevation.html
20 | */
21 |
22 | --md-grey-900: #212121;
23 | --md-grey-400: #bdbdbd;
24 | --md-grey-200: #eeeeee;
25 | --md-blue-500: #2196f3;
26 |
27 |
28 | /* The dark theme shadows need a bit of work, but this will probably also require work on the core layout
29 | * colors used in the theme as well. */
30 | --jp-shadow-base-lightness: 32;
31 | --jp-shadow-umbra-color: rgba(
32 | var(--jp-shadow-base-lightness),
33 | var(--jp-shadow-base-lightness),
34 | var(--jp-shadow-base-lightness),
35 | 0.2
36 | );
37 | --jp-shadow-penumbra-color: rgba(
38 | var(--jp-shadow-base-lightness),
39 | var(--jp-shadow-base-lightness),
40 | var(--jp-shadow-base-lightness),
41 | 0.14
42 | );
43 | --jp-shadow-ambient-color: rgba(
44 | var(--jp-shadow-base-lightness),
45 | var(--jp-shadow-base-lightness),
46 | var(--jp-shadow-base-lightness),
47 | 0.12
48 | );
49 | --jp-elevation-z0: none;
50 | --jp-elevation-z1: 0px 2px 1px -1px var(--jp-shadow-umbra-color),
51 | 0px 1px 1px 0px var(--jp-shadow-penumbra-color),
52 | 0px 1px 3px 0px var(--jp-shadow-ambient-color);
53 | --jp-elevation-z2: 0px 3px 1px -2px var(--jp-shadow-umbra-color),
54 | 0px 2px 2px 0px var(--jp-shadow-penumbra-color),
55 | 0px 1px 5px 0px var(--jp-shadow-ambient-color);
56 | --jp-elevation-z4: 0px 2px 4px -1px var(--jp-shadow-umbra-color),
57 | 0px 4px 5px 0px var(--jp-shadow-penumbra-color),
58 | 0px 1px 10px 0px var(--jp-shadow-ambient-color);
59 | --jp-elevation-z6: 0px 3px 5px -1px var(--jp-shadow-umbra-color),
60 | 0px 6px 10px 0px var(--jp-shadow-penumbra-color),
61 | 0px 1px 18px 0px var(--jp-shadow-ambient-color);
62 | --jp-elevation-z8: 0px 5px 5px -3px var(--jp-shadow-umbra-color),
63 | 0px 8px 10px 1px var(--jp-shadow-penumbra-color),
64 | 0px 3px 14px 2px var(--jp-shadow-ambient-color);
65 | --jp-elevation-z12: 0px 7px 8px -4px var(--jp-shadow-umbra-color),
66 | 0px 12px 17px 2px var(--jp-shadow-penumbra-color),
67 | 0px 5px 22px 4px var(--jp-shadow-ambient-color);
68 | --jp-elevation-z16: 0px 8px 10px -5px var(--jp-shadow-umbra-color),
69 | 0px 16px 24px 2px var(--jp-shadow-penumbra-color),
70 | 0px 6px 30px 5px var(--jp-shadow-ambient-color);
71 | --jp-elevation-z20: 0px 10px 13px -6px var(--jp-shadow-umbra-color),
72 | 0px 20px 31px 3px var(--jp-shadow-penumbra-color),
73 | 0px 8px 38px 7px var(--jp-shadow-ambient-color);
74 | --jp-elevation-z24: 0px 11px 15px -7px var(--jp-shadow-umbra-color),
75 | 0px 24px 38px 3px var(--jp-shadow-penumbra-color),
76 | 0px 9px 46px 8px var(--jp-shadow-ambient-color);
77 |
78 | /* Borders
79 | *
80 | * The following variables, specify the visual styling of borders in JupyterLab.
81 | */
82 |
83 | --jp-border-width: 1px;
84 | --jp-border-radius: 2px;
85 |
86 | /* UI Fonts
87 | *
88 | * The UI font CSS variables are used for the typography all of the JupyterLab
89 | * user interface elements that are not directly user generated content.
90 | *
91 | * The font sizing here is done assuming that the body font size of --jp-ui-font-size1
92 | * is applied to a parent element. When children elements, such as headings, are sized
93 | * in em all things will be computed relative to that body size.
94 | */
95 |
96 | --jp-ui-font-scale-factor: 1.2;
97 | --jp-ui-font-size0: 0.83333em;
98 | --jp-ui-font-size1: 13px; /* Base font size */
99 | --jp-ui-font-size2: 1.2em;
100 | --jp-ui-font-size3: 1.44em;
101 |
102 | --jp-ui-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica,
103 | Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
104 |
105 | /*
106 | * Use these font colors against the corresponding main layout colors.
107 | * In a light theme, these go from dark to light.
108 | */
109 |
110 | /* Defaults use Material Design specification */
111 | --jp-ui-font-color0: rgba(0, 0, 0, 1);
112 | --jp-ui-font-color1: rgba(0, 0, 0, 0.87);
113 | --jp-ui-font-color2: rgba(0, 0, 0, 0.54);
114 | --jp-ui-font-color3: rgba(0, 0, 0, 0.38);
115 |
116 | /*
117 | * Use these against the brand/accent/warn/error colors.
118 | * These will typically go from light to darker, in both a dark and light theme.
119 | */
120 |
121 | --jp-ui-inverse-font-color0: rgba(0, 0, 0, 1);
122 | --jp-ui-inverse-font-color1: rgba(0, 0, 0, 0.8);
123 | --jp-ui-inverse-font-color2: rgba(0, 0, 0, 0.5);
124 | --jp-ui-inverse-font-color3: rgba(0, 0, 0, 0.3);
125 |
126 | /* Content Fonts
127 | *
128 | * Content font variables are used for typography of user generated content.
129 | *
130 | * The font sizing here is done assuming that the body font size of --jp-content-font-size1
131 | * is applied to a parent element. When children elements, such as headings, are sized
132 | * in em all things will be computed relative to that body size.
133 | */
134 |
135 | --jp-content-line-height: 1.6;
136 | --jp-content-font-scale-factor: 1.2;
137 | --jp-content-font-size0: 0.83333em;
138 | --jp-content-font-size1: 14px; /* Base font size */
139 | --jp-content-font-size2: 1.2em;
140 | --jp-content-font-size3: 1.44em;
141 | --jp-content-font-size4: 1.728em;
142 | --jp-content-font-size5: 2.0736em;
143 |
144 | /* This gives a magnification of about 125% in presentation mode over normal. */
145 | --jp-content-presentation-font-size1: 17px;
146 |
147 | --jp-content-heading-line-height: 1;
148 | --jp-content-heading-margin-top: 1.2em;
149 | --jp-content-heading-margin-bottom: 0.8em;
150 | --jp-content-heading-font-weight: 500;
151 |
152 | /* Defaults use Material Design specification */
153 | --jp-content-font-color0: rgba(0, 0, 0, 1);
154 | --jp-content-font-color1: rgba(0, 0, 0, 0.87);
155 | --jp-content-font-color2: rgba(0, 0, 0, 0.54);
156 | --jp-content-font-color3: rgba(0, 0, 0, 0.38);
157 |
158 |
159 | --jp-content-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI',
160 | Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
161 | 'Segoe UI Symbol';
162 |
163 | /*
164 | * Code Fonts
165 | *
166 | * Code font variables are used for typography of code and other monospaces content.
167 | */
168 |
169 | --jp-code-font-size: 13px;
170 | --jp-code-line-height: 1.3077; /* 17px for 13px base */
171 | --jp-code-padding: 5px; /* 5px for 13px base, codemirror highlighting needs integer px value */
172 | --jp-code-font-family-default: Menlo, Consolas, 'DejaVu Sans Mono', monospace;
173 | --jp-code-font-family: var(--jp-code-font-family-default);
174 |
175 | /* This gives a magnification of about 125% in presentation mode over normal. */
176 | --jp-code-presentation-font-size: 16px;
177 |
178 | /* may need to tweak cursor width if you change font size */
179 | --jp-code-cursor-width0: 1.4px;
180 | --jp-code-cursor-width1: 2px;
181 | --jp-code-cursor-width2: 4px;
182 |
183 | /* Layout
184 | *
185 | * The following are the main layout colors use in JupyterLab. In a light
186 | * theme these would go from light to dark.
187 | */
188 |
189 | --jp-layout-color0: #111111;
190 |
191 | /* Inverse Layout
192 | *
193 | * The following are the inverse layout colors use in JupyterLab. In a light
194 | * theme these would go from dark to light.
195 | */
196 |
197 | --jp-inverse-layout-color0: white;
198 | --jp-inverse-layout-color1: white;
199 |
200 | /* Brand/accent */
201 |
202 |
203 | /* State colors (warn, error, success, info) */
204 |
205 |
206 | /* Cell specific styles */
207 |
208 | --jp-cell-padding: 5px;
209 |
210 | --jp-cell-collapser-width: 8px;
211 | --jp-cell-collapser-min-height: 20px;
212 | --jp-cell-collapser-not-active-hover-opacity: 0.6;
213 |
214 | --jp-cell-editor-active-background: var(--jp-layout-color0);
215 |
216 | --jp-cell-prompt-width: 64px;
217 | --jp-cell-prompt-font-family: 'Source Code Pro', monospace;
218 | --jp-cell-prompt-letter-spacing: 0px;
219 | --jp-cell-prompt-opacity: 1;
220 | --jp-cell-prompt-not-active-opacity: 1;
221 |
222 | /* A custom blend of MD grey and blue 600
223 | * See https://meyerweb.com/eric/tools/color-blend/#546E7A:1E88E5:5:hex */
224 | --jp-cell-inprompt-font-color: #307fc1;
225 | /* A custom blend of MD grey and orange 600
226 | * https://meyerweb.com/eric/tools/color-blend/#546E7A:F4511E:5:hex */
227 | --jp-cell-outprompt-font-color: #bf5b3d;
228 |
229 | /* Notebook specific styles */
230 |
231 | --jp-notebook-padding: 10px;
232 | --jp-notebook-multiselected-color: rgba(33, 150, 243, 0.24);
233 |
234 | /* The scroll padding is calculated to fill enough space at the bottom of the
235 | notebook to show one single-line cell (with appropriate padding) at the top
236 | when the notebook is scrolled all the way to the bottom. We also subtract one
237 | pixel so that no scrollbar appears if we have just one single-line cell in the
238 | notebook. This padding is to enable a 'scroll past end' feature in a notebook.
239 | */
240 | --jp-notebook-scroll-padding: calc(
241 | 100% - var(--jp-code-font-size) * var(--jp-code-line-height) -
242 | var(--jp-code-padding) - var(--jp-cell-padding) - 1px
243 | );
244 |
245 | /* Rendermime styles */
246 |
247 | --jp-rendermime-error-background: rgba(244, 67, 54, 0.28);
248 | --jp-rendermime-table-row-hover-background: rgba(3, 169, 244, 0.2);
249 |
250 | /* Dialog specific styles */
251 |
252 | --jp-dialog-background: rgba(0, 0, 0, 0.6);
253 |
254 | /* Console specific styles */
255 |
256 | --jp-console-padding: 10px;
257 |
258 | /* Toolbar specific styles */
259 |
260 | --jp-toolbar-micro-height: 8px;
261 | --jp-toolbar-box-shadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.8);
262 | --jp-toolbar-header-margin: 4px 4px 0px 4px;
263 | --jp-toolbar-active-background: var(--jp-layout-color0);
264 |
265 | /* Input field styles */
266 |
267 | --jp-input-active-background: var(--jp-layout-color0);
268 | --jp-input-active-box-shadow-color: rgba(19, 124, 189, 0.3);
269 |
270 | /* General editor styles */
271 |
272 | --jp-editor-selected-focused-background: rgba(33, 150, 243, 0.24);
273 | --jp-editor-cursor-color: var(--jp-ui-font-color0);
274 |
275 | /* Code mirror specific styles */
276 |
277 | --jp-mirror-editor-operator-color: #aa22ff;
278 | --jp-mirror-editor-comment-color: #408080;
279 | --jp-mirror-editor-string-color: #ba2121;
280 | --jp-mirror-editor-meta-color: #aa22ff;
281 | --jp-mirror-editor-qualifier-color: #555;
282 | --jp-mirror-editor-bracket-color: #997;
283 | --jp-mirror-editor-error-color: #f00;
284 | --jp-mirror-editor-hr-color: #999;
285 |
286 | /* Sidebar-related styles */
287 |
288 | --jp-sidebar-min-width: 180px;
289 |
290 | /* Search-related styles */
291 |
292 | --jp-search-toggle-off-opacity: 0.6;
293 | --jp-search-toggle-hover-opacity: 0.8;
294 | --jp-search-toggle-on-opacity: 1;
295 | --jp-search-selected-match-background-color: rgb(255, 225, 0);
296 | --jp-search-selected-match-color: black;
297 | --jp-search-unselected-match-background-color: var(
298 | --jp-inverse-layout-color0
299 | );
300 | --jp-search-unselected-match-color: var(--jp-ui-inverse-font-color0);
301 |
302 | /* scrollbar related styles. Supports every browser except Edge. */
303 |
304 | /* colors based on JetBrain's Darcula theme */
305 |
306 | --jp-scrollbar-background-color: #3f4244;
307 | --jp-scrollbar-thumb-color: 88, 96, 97; /* need to specify thumb color as an RGB triplet */
308 |
309 | --jp-scrollbar-endpad: 3px; /* the minimum gap between the thumb and the ends of a scrollbar */
310 |
311 | /* hacks for setting the thumb shape. These do nothing in Firefox */
312 |
313 | --jp-scrollbar-thumb-margin: 3.5px; /* the space in between the sides of the thumb and the track */
314 | --jp-scrollbar-thumb-radius: 9px; /* set to a large-ish value for rounded endcaps on the thumb */
315 |
316 |
317 | --jp-layout-color1: white;
318 | --jp-border-color1: var(--md-grey-400);
319 | --jp-layout-color2: var(--md-grey-200);
320 | --jp-brand-color1: var(--md-blue-500);
321 |
322 | --jp-private-completer-item-height: 22px;
323 | /* Shift the baseline of the type character to align with the match text */
324 | --jp-private-completer-type-offset: 2px;
325 | }
326 |
327 | #site {
328 | position: relative !important;
329 | }
330 |
331 | .completions {
332 | /*position: relative !important;*/
333 | }
334 |
335 | .jp-Completer {
336 | box-shadow: var(--jp-elevation-z6);
337 | background: var(--jp-layout-color1);
338 | color: var(--jp-content-font-color1);
339 | border: var(--jp-border-width) solid var(--jp-border-color1);
340 | list-style-type: none;
341 | overflow-y: scroll;
342 | overflow-x: auto;
343 | padding: 0;
344 | /* Position the completer relative to the text editor, align the '.' */
345 | margin: 0 0 0 0;
346 | max-height: calc(
347 | (10 * var(--jp-private-completer-item-height)) +
348 | (2 * var(--jp-border-width))
349 | );
350 | min-height: calc(
351 | var(--jp-private-completer-item-height) + (2 * var(--jp-border-width))
352 | );
353 | z-index: 10001;
354 | }
355 |
356 | .jp-Completer-item {
357 | display: table-row;
358 | box-sizing: border-box;
359 | margin: 0;
360 | padding: 0;
361 | height: var(--jp-private-completer-item-height);
362 | min-width: 150px;
363 | }
364 |
365 | .jp-Completer-item .jp-Completer-match {
366 | display: table-cell;
367 | box-sizing: border-box;
368 | margin: 0;
369 | padding: 0 8px 0 6px;
370 | height: var(--jp-private-completer-item-height);
371 | font-family: var(--jp-code-font-family);
372 | font-size: var(--jp-code-font-size);
373 | line-height: var(--jp-private-completer-item-height);
374 | }
375 |
376 | .jp-Completer-item .jp-Completer-match.jp-Completer-deprecated {
377 | text-decoration: line-through;
378 | }
379 |
380 | .jp-Completer-item .jp-Completer-type {
381 | display: table-cell;
382 | box-sizing: border-box;
383 | height: var(--jp-private-completer-item-height);
384 | text-align: center;
385 | background: transparent;
386 | color: white;
387 | width: var(--jp-private-completer-item-height);
388 | font-family: var(--jp-ui-font-family);
389 | font-size: var(--jp-ui-font-size1);
390 | line-height: calc(
391 | var(--jp-private-completer-item-height) -
392 | var(--jp-private-completer-type-offset)
393 | );
394 | padding-bottom: var(--jp-private-completer-type-offset);
395 | }
396 |
397 | .jp-Completer-item .jp-Completer-typeExtended {
398 | display: table-cell;
399 | box-sizing: border-box;
400 | height: var(--jp-private-completer-item-height);
401 | text-align: right;
402 | background: transparent;
403 | color: var(--jp-ui-font-color2);
404 | font-family: var(--jp-code-font-family);
405 | font-size: var(--jp-code-font-size);
406 | line-height: var(--jp-private-completer-item-height);
407 | padding-right: 8px;
408 | }
409 |
410 | .jp-Completer-item:hover {
411 | background: var(--jp-layout-color2);
412 | opacity: 0.8;
413 | }
414 |
415 | .jp-Completer-item.jp-mod-active {
416 | background: var(--jp-brand-color1);
417 | color: white;
418 | }
419 |
420 | .jp-Completer-item .jp-Completer-match mark {
421 | font-weight: bold;
422 | background: inherit;
423 | color: inherit;
424 | }
425 |
426 | .jp-Completer-type[data-color-index='0'] {
427 | background: transparent;
428 | }
429 |
430 | .jp-Completer-type[data-color-index='1'] {
431 | background: #1f77b4;
432 | }
433 |
434 | .jp-Completer-type[data-color-index='2'] {
435 | background: #ff7f0e;
436 | }
437 |
438 | .jp-Completer-type[data-color-index='3'] {
439 | background: #2ca02c;
440 | }
441 |
442 | .jp-Completer-type[data-color-index='4'] {
443 | background: #d62728;
444 | }
445 |
446 | .jp-Completer-type[data-color-index='5'] {
447 | background: #9467bd;
448 | }
449 |
450 | .jp-Completer-type[data-color-index='6'] {
451 | background: #8c564b;
452 | }
453 |
454 | .jp-Completer-type[data-color-index='7'] {
455 | background: #e377c2;
456 | }
457 |
458 | .jp-Completer-type[data-color-index='8'] {
459 | background: #7f7f7f;
460 | }
461 |
462 | .jp-Completer-type[data-color-index='9'] {
463 | background: #bcbd22;
464 | }
465 |
466 | .jp-Completer-type[data-color-index='10'] {
467 | background: #17becf;
468 | }
469 |
470 | .cm__red_wavy_line {
471 | background: url("./wavyline-red.gif") repeat-x 100% 100%;
472 | padding-bottom: 2px;
473 | }
474 |
475 | .cm__orange_wavy_line {
476 | background: url("./wavyline-orange.gif") repeat-x 100% 100%;
477 | padding-bottom: 2px;
478 | }
479 |
480 | .kotlin-error-tooltip {
481 | border: 1px solid silver;
482 | border-radius: 3px;
483 | color: #444;
484 | padding: 2px 5px;
485 | font-size: 90%;
486 | font-family: monospace;
487 | background-color: white;
488 | white-space: pre-wrap;
489 |
490 | max-width: 40em;
491 | position: absolute;
492 | z-index: 10;
493 | -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
494 | -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
495 | box-shadow: 2px 3px 5px rgba(0,0,0,.2);
496 |
497 | transition: opacity 1s;
498 | -moz-transition: opacity 1s;
499 | -webkit-transition: opacity 1s;
500 | -o-transition: opacity 1s;
501 | -ms-transition: opacity 1s;
502 | }
503 |
--------------------------------------------------------------------------------
/ghidra_jupyter/kernel/kernel.js:
--------------------------------------------------------------------------------
1 | /*
2 | This file includes code of Jupyter notebook project (https://github.com/jupyter/notebook)
3 | which is licensed under the terms of the Modified BSD License
4 | (also known as New or Revised or 3-Clause BSD), as follows:
5 |
6 | - Copyright (c) 2001-2015, IPython Development Team
7 | - Copyright (c) 2015-, Jupyter Development Team
8 |
9 | All rights reserved.
10 |
11 | Full license text is available in additional-licenses/LICENSE_BSD_3 file
12 | */
13 |
14 | define(function(){
15 | function onload() {
16 | if (!Element.prototype.scrollIntoViewIfNeeded) {
17 | Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded) {
18 | centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded;
19 |
20 | var parent = this.parentNode,
21 | parentComputedStyle = window.getComputedStyle(parent, null),
22 | parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')),
23 | parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')),
24 | overTop = this.offsetTop - parent.offsetTop < parent.scrollTop,
25 | overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight),
26 | overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft,
27 | overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth),
28 | alignWithTop = overTop && !overBottom;
29 |
30 | if ((overTop || overBottom) && centerIfNeeded) {
31 | parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2;
32 | }
33 |
34 | if ((overLeft || overRight) && centerIfNeeded) {
35 | parent.scrollLeft = this.offsetLeft - parent.offsetLeft - parent.clientWidth / 2 - parentBorderLeftWidth + this.clientWidth / 2;
36 | }
37 |
38 | if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) {
39 | this.scrollIntoView(alignWithTop);
40 | }
41 | };
42 | }
43 |
44 | var utils = require('base/js/utils');
45 | var keyboard = require('base/js/keyboard');
46 | var $ = require('jquery');
47 | var CodeMirror = require('codemirror/lib/codemirror');
48 | var Completer = requirejs("notebook/js/completer").Completer;
49 | var Cell = requirejs("notebook/js/cell").Cell;
50 | var CodeCell = requirejs("notebook/js/codecell").CodeCell;
51 | var Kernel = requirejs("services/kernels/kernel").Kernel;
52 |
53 | var error_class = "cm__red_wavy_line";
54 | var warning_class = "cm__orange_wavy_line";
55 | var additionalClasses = [error_class, warning_class];
56 | var diag_class = {
57 | "ERROR": error_class,
58 | "WARNING": warning_class
59 | };
60 |
61 | function rectIntersect(a, b) {
62 | return (a.left <= b.right &&
63 | b.left <= a.right &&
64 | a.top <= b.bottom &&
65 | b.top <= a.bottom)
66 | }
67 |
68 | function isOnScreen(elem) {
69 | var bounding = elem.getBoundingClientRect();
70 | return rectIntersect(
71 | bounding,
72 | {
73 | top: 0,
74 | left: 0,
75 | bottom: (window.innerHeight || document.documentElement.clientHeight),
76 | right: (window.innerWidth || document.documentElement.clientWidth)
77 | }
78 | )
79 | }
80 |
81 | var opened_completer = null;
82 | var siteEl = document.getElementById("site");
83 | $(siteEl).scroll((e) => {
84 | if (opened_completer && !isOnScreen(opened_completer.complete[0])) {
85 | opened_completer.close();
86 | }
87 | });
88 |
89 | var cssUrl = require.toUrl(Jupyter.kernelselector.kernelspecs.kotlin.resources["kernel.css"]);
90 | $('head').append('');
91 |
92 | var keycodes = keyboard.keycodes;
93 |
94 | var prepend_n_prc = function(str, n) {
95 | for( var i =0 ; i< n ; i++){
96 | str = '%'+str ;
97 | }
98 | return str;
99 | };
100 |
101 | var _existing_completion = function(item, completion_array){
102 | for( var i=0; i < completion_array.length; i++) {
103 | if (completion_array[i].trim().substr(-item.length) == item) {
104 | return true;
105 | }
106 | }
107 | return false;
108 | };
109 |
110 | // what is the common start of all completions
111 | function shared_start(B, drop_prct) {
112 | if (B.length == 1) {
113 | return B[0];
114 | }
115 | var A = [];
116 | var common;
117 | var min_lead_prct = 10;
118 | for (var i = 0; i < B.length; i++) {
119 | var str = B[i].replaceText;
120 | var localmin = 0;
121 | if(drop_prct === true){
122 | while ( str.substr(0, 1) == '%') {
123 | localmin = localmin+1;
124 | str = str.substring(1);
125 | }
126 | }
127 | min_lead_prct = Math.min(min_lead_prct, localmin);
128 | A.push(str);
129 | }
130 |
131 | if (A.length > 1) {
132 | var tem1, tem2, s;
133 | A = A.slice(0).sort();
134 | tem1 = A[0];
135 | s = tem1.length;
136 | tem2 = A.pop();
137 | while (s && tem2.indexOf(tem1) == -1) {
138 | tem1 = tem1.substring(0, --s);
139 | }
140 | if (tem1 === "" || tem2.indexOf(tem1) !== 0) {
141 | return {
142 | replaceText: prepend_n_prc('', min_lead_prct),
143 | type: "computed",
144 | from: B[0].from,
145 | to: B[0].to
146 | };
147 | }
148 | return {
149 | replaceText: prepend_n_prc(tem1, min_lead_prct),
150 | type: "computed",
151 | from: B[0].from,
152 | to: B[0].to
153 | };
154 | }
155 | return null;
156 | }
157 |
158 | Completer.prototype.startCompletion = function (doAutoPrint) {
159 | /**
160 | * call for a 'first' completion, that will set the editor and do some
161 | * special behavior like autopicking if only one completion available.
162 | */
163 | this.do_auto_print = !!doAutoPrint;
164 | if (this.editor.somethingSelected()|| this.editor.getSelections().length > 1) return;
165 | this.done = false;
166 | // use to get focus back on opera
167 | this.carry_on_completion(true);
168 | };
169 |
170 | function indexOf(str, filter, from, to, step) {
171 | var cond = (from < to) ? (j => j <= to) : (j => j >= to);
172 | for (var i = from; cond(i); i += step) {
173 | if (filter(str[i]))
174 | return i;
175 | }
176 | return -1;
177 | }
178 |
179 | function getTokenBounds(buf, cursor, editor) {
180 | if (cursor > buf.length) {
181 | throw new Error("Position " + cursor + " does not exist in code snippet <" + buf + ">");
182 | }
183 |
184 | var filter = c => !/^[A-Z0-9_]$/i.test(c);
185 |
186 | var start = indexOf(buf, filter, cursor - 1, 0, -1) + 1;
187 | var end = cursor
188 |
189 | return {
190 | before: buf.substring(0, start),
191 | token: buf.substring(start, end),
192 | tokenBeforeCursor: buf.substring(start, cursor),
193 | after: buf.substring(end, buf.length),
194 | start: start,
195 | end: end,
196 | posStart: editor.posFromIndex(start),
197 | posEnd: editor.posFromIndex(end)
198 | }
199 | }
200 |
201 | Completer.prototype.carry_on_completion = function (first_invocation) {
202 | /**
203 | * Pass true as parameter if you want the completer to autopick when
204 | * only one completion. This function is automatically reinvoked at
205 | * each keystroke with first_invocation = false
206 | */
207 | var cur = this.editor.getCursor();
208 | var line = this.editor.getLine(cur.line);
209 | var pre_cursor = this.editor.getRange({
210 | line: cur.line,
211 | ch: cur.ch - 1
212 | }, cur);
213 |
214 | // we need to check that we are still on a word boundary
215 | // because while typing the completer is still reinvoking itself
216 | // so dismiss if we are on a "bad" character
217 | if (!this.reinvoke(pre_cursor) && !first_invocation) {
218 | this.close();
219 | return;
220 | }
221 |
222 | this.autopick = !!first_invocation;
223 |
224 | // We want a single cursor position.
225 | if (this.editor.somethingSelected()|| this.editor.getSelections().length > 1) {
226 | return;
227 | }
228 |
229 | var cursor_pos = this.editor.indexFromPos(cur);
230 | var text = this.editor.getValue();
231 | cursor_pos = utils.js_idx_to_char_idx(cursor_pos, text);
232 |
233 | var prevBounds = this.tokenBounds;
234 | var bounds = getTokenBounds(text, cursor_pos, this.editor);
235 | this.tokenBounds = bounds;
236 | if (prevBounds && this.raw_result) {
237 | if (bounds.before === prevBounds.before &&
238 | bounds.after === prevBounds.after &&
239 | bounds.end > prevBounds.end) {
240 |
241 | var newResult = this.raw_result.filter((v) => {
242 | var displayName = v.str;
243 | if (displayName[0] === '`')
244 | displayName = displayName.substring(1, displayName.length - 1);
245 | return displayName.startsWith(bounds.tokenBeforeCursor)
246 | }).map((completion) => {
247 | completion.from = bounds.posStart;
248 | completion.to = bounds.posEnd;
249 | return completion;
250 | });
251 |
252 | if (newResult.length > 0) {
253 | this.raw_result = newResult;
254 | this.make_gui(this.prepare_cursor_pos(bounds.start, cursor_pos));
255 | return;
256 | }
257 | }
258 | }
259 |
260 | // one kernel completion came back, finish_completing will be called with the results
261 | // we fork here and directly call finish completing if kernel is busy
262 |
263 | if (this.skip_kernel_completion) {
264 | this.finish_completing({ content: {
265 | matches: [],
266 | cursor_start: cursor_pos,
267 | cursor_end: cursor_pos,
268 | }});
269 | } else {
270 | this.cell.kernel.complete(text, cursor_pos,
271 | $.proxy(this.finish_completing, this)
272 | );
273 | }
274 | };
275 |
276 | function convertPos(pos) {
277 | return {
278 | line: pos.line - 1,
279 | ch: pos.col - 1
280 | }
281 | }
282 |
283 | function adjustRange(start, end) {
284 | var s = convertPos(start);
285 | var e = convertPos(end);
286 | if (s.line === e.line && s.ch === e.ch) {
287 | s.ch --;
288 | }
289 | return {
290 | start: s,
291 | end: e
292 | }
293 | }
294 |
295 | function highlightErrors (errors, cell) {
296 | errors = errors || [];
297 | for (let error of errors) {
298 | var start = error.start;
299 | var end = error.end;
300 | if (!start || !end)
301 | continue;
302 | var r = adjustRange(start, end);
303 | error.range = r;
304 | var cl = diag_class[error.severity];
305 | cell.code_mirror.markText(r.start, r.end, {className: cl});
306 | }
307 |
308 | cell.errorsList = errors;
309 | }
310 |
311 | Cell.prototype.highlightErrors = function (errors) {
312 | highlightErrors(errors, this)
313 | };
314 |
315 | Completer.prototype.make_gui = function(cur_pos, cur) {
316 | var start = cur_pos.start;
317 | cur = cur || this.editor.getCursor();
318 |
319 | // if empty result return
320 | if (!this.raw_result || !this.raw_result.length) {
321 | this.close();
322 | return;
323 | }
324 |
325 | // When there is only one completion, use it directly.
326 | if (this.do_auto_print && this.autopick && this.raw_result.length == 1) {
327 | this.insert(this.raw_result[0]);
328 | return;
329 | }
330 |
331 | if (this.raw_result.length == 1) {
332 | // test if first and only completion totally matches
333 | // what is typed, in this case dismiss
334 | var str = this.raw_result[0].str;
335 | var pre_cursor = this.editor.getRange({
336 | line: cur.line,
337 | ch: cur.ch - str.length
338 | }, cur);
339 | if (pre_cursor == str) {
340 | this.close();
341 | return;
342 | }
343 | }
344 |
345 | if (!this.visible) {
346 | this.complete = $('').addClass('completions');
347 | this.complete.attr('id', 'complete');
348 |
349 | // Currently webkit doesn't use the size attr correctly. See:
350 | // https://code.google.com/p/chromium/issues/detail?id=4579
351 | this.sel = $('')
352 | .attr('tabindex', -1)
353 | .attr('multiple', 'true')
354 | .addClass('jp-Completer');
355 | this.complete.append(this.sel);
356 | this.visible = true;
357 | $(siteEl).append(this.complete);
358 |
359 | //build the container
360 | var that = this;
361 | this._handle_keydown = function (cm, event) {
362 | that.keydown(event);
363 | };
364 | this.editor.on('keydown', this._handle_keydown);
365 | this._handle_keypress = function (cm, event) {
366 | that.keypress(event);
367 | };
368 | this.editor.on('keypress', this._handle_keypress);
369 | }
370 | this.sel.attr('size', Math.min(10, this.raw_result.length));
371 |
372 | // Clear and fill the list.
373 | this.sel.text('');
374 | this.build_gui_list(this.raw_result);
375 |
376 | // After everything is on the page, compute the position.
377 | // We put it above the code if it is too close to the bottom of the page.
378 | var pos = this.editor.cursorCoords(
379 | this.editor.posFromIndex(start)
380 | );
381 | var left = pos.left-3;
382 | var top;
383 | var cheight = this.complete.height();
384 | var wheight = $(window).height();
385 | if (pos.bottom+cheight+5 > wheight) {
386 | top = pos.top-cheight-4;
387 | } else {
388 | top = pos.bottom+1;
389 | }
390 |
391 | this.complete.css('left', left + siteEl.scrollLeft - siteEl.offsetLeft + 'px');
392 | this.complete.css('top', top + siteEl.scrollTop - siteEl.offsetTop + 'px');
393 |
394 | opened_completer = this;
395 | return true;
396 | };
397 |
398 | Completer.prototype.prepare_cursor_pos = function(start, end) {
399 | if (end === null) {
400 | // adapted message spec replies don't have cursor position info,
401 | // interpret end=null as current position,
402 | // and negative start relative to that
403 | end = this.editor.indexFromPos(cur);
404 | if (start === null) {
405 | start = end;
406 | } else if (start < 0) {
407 | start = end + start;
408 | }
409 | } else {
410 | // handle surrogate pairs
411 | var text = this.editor.getValue();
412 | end = utils.char_idx_to_js_idx(end, text);
413 | start = utils.char_idx_to_js_idx(start, text);
414 | }
415 |
416 | return {
417 | start: start,
418 | end: end
419 | }
420 | };
421 |
422 | Completer.prototype.finish_completing = function (msg) {
423 | /**
424 | * let's build a function that wrap all that stuff into what is needed
425 | * for the new completer:
426 | */
427 |
428 | // alert("WOW");
429 |
430 | var content = msg.content;
431 | var start = content.cursor_start;
432 | var end = content.cursor_end;
433 | var matches = content.matches;
434 | var metadata = content.metadata || {};
435 | var extMetadata = metadata._jupyter_extended_metadata || {};
436 |
437 | console.log(content);
438 |
439 | // If completion error occurs
440 | if (matches === undefined) {
441 | return;
442 | }
443 |
444 | var cur = this.editor.getCursor();
445 |
446 | var paragraph = content.paragraph;
447 | if (paragraph) {
448 | if (paragraph.cursor !== this.editor.indexFromPos(cur)
449 | || paragraph.text !== this.editor.getValue()) {
450 | // this.close();
451 | return;
452 | }
453 | }
454 |
455 | var newPos = this.prepare_cursor_pos(start, end);
456 | start = newPos.start;
457 | end = newPos.end;
458 |
459 | var filtered_results = [];
460 | //remove results from context completion
461 | //that are already in kernel completion
462 | var i;
463 |
464 | // append the introspection result, in order, at at the beginning of
465 | // the table and compute the replacement range from current cursor
466 | // position and matched_text length.
467 | var from = this.editor.posFromIndex(start);
468 | var to = this.editor.posFromIndex(end);
469 | for (i = matches.length - 1; i >= 0; --i) {
470 | var info = extMetadata[i] || {};
471 | var replaceText = info.text || matches[i];
472 | var displayText = info.displayText || replaceText;
473 |
474 | filtered_results.unshift({
475 | str: displayText,
476 | replaceText: replaceText,
477 | tail: info.tail,
478 | icon: info.icon,
479 | type: "introspection",
480 | deprecation: info.deprecation,
481 | from: from,
482 | to: to
483 | });
484 | }
485 |
486 | // one the 2 sources results have been merge, deal with it
487 | this.raw_result = filtered_results;
488 |
489 | this.make_gui(newPos, cur);
490 | };
491 |
492 | Completer.prototype.pickColor = function(iconText) {
493 | this.colorsDict = this.colorsDict || {};
494 | var colorInd = this.colorsDict[iconText];
495 | if (colorInd) {
496 | return colorInd;
497 | }
498 | colorInd = Math.floor(Math.random() * 10) + 1;
499 | this.colorsDict[iconText] = colorInd;
500 | return colorInd;
501 | };
502 |
503 | Completer.prototype.insert = function (completion) {
504 | this.editor.replaceRange(completion.replaceText, completion.from, completion.to);
505 | };
506 |
507 | Completer.prototype.build_gui_list = function (completions) {
508 | var MAXIMUM_GUI_LIST_LENGTH = 1000;
509 | var that = this;
510 | for (var i = 0; i < completions.length && i < MAXIMUM_GUI_LIST_LENGTH; ++i) {
511 | var comp = completions[i];
512 |
513 | var icon = comp.icon || 'u';
514 | var text = comp.replaceText || '';
515 | var displayText = comp.str || '';
516 | var tail = comp.tail || '';
517 |
518 | var typeColorIndex = this.pickColor(icon);
519 | var iconTag = $('')
520 | .text(icon.charAt(0))
521 | .addClass('jp-Completer-type')
522 | .attr('data-color-index', typeColorIndex);
523 |
524 | var matchTag = $('').text(displayText).addClass('jp-Completer-match');
525 | if (comp.deprecation != null) {
526 | matchTag.addClass('jp-Completer-deprecated');
527 | }
528 |
529 | var typeTag = $('').text(tail).addClass('jp-Completer-typeExtended');
530 |
531 | var opt = $('').addClass('jp-Completer-item');
532 | opt.click((function (k, event) {
533 | this.selIndex = k;
534 | this.pick();
535 | this.editor.focus();
536 | }).bind(that, i));
537 |
538 | opt.append(iconTag, matchTag, typeTag);
539 |
540 | this.sel.append(opt);
541 | }
542 | this.sel.children().first().addClass('jp-mod-active');
543 | this.selIndex = 0;
544 | // this.sel.scrollTop(0);
545 | };
546 |
547 | Completer.prototype.close = function () {
548 | this.done = true;
549 | opened_completer = null;
550 | $('#complete').remove();
551 | this.editor.off('keydown', this._handle_keydown);
552 | this.editor.off('keypress', this._handle_keypress);
553 | this.visible = false;
554 | };
555 |
556 | Completer.prototype.pick = function () {
557 | var ind = this.selIndex;
558 | var completion = this.raw_result[ind];
559 | this.insert(completion);
560 | this.close();
561 | };
562 |
563 | Completer.prototype.selectNew = function(newIndex) {
564 | $(this.sel.children()[this.selIndex]).removeClass('jp-mod-active');
565 | this.selIndex = newIndex;
566 |
567 | var active = this.sel.children()[this.selIndex];
568 | $(active).addClass('jp-mod-active');
569 | active.scrollIntoViewIfNeeded(false);
570 | };
571 |
572 | Completer.prototype.keydown = function (event) {
573 | var code = event.keyCode;
574 |
575 | // Enter
576 | var optionsLen;
577 | var index;
578 | var prevIndex;
579 | if (code == keycodes.enter && !(event.shiftKey || event.ctrlKey || event.metaKey || event.altKey)) {
580 | event.codemirrorIgnore = true;
581 | event._ipkmIgnore = true;
582 | event.preventDefault();
583 | this.pick();
584 | // Escape or backspace
585 | } else if (code == keycodes.esc) {
586 | event.codemirrorIgnore = true;
587 | event._ipkmIgnore = true;
588 | event.preventDefault();
589 | this.close();
590 | } else if (code == keycodes.tab) {
591 | //all the fastforwarding operation,
592 | //Check that shared start is not null which can append with prefixed completion
593 | // like %pylab , pylab have no shred start, and ff will result in py
594 | // to erase py
595 |
596 | var sh = shared_start(this.raw_result, true);
597 | if (sh.str !== '') {
598 | this.insert(sh);
599 | }
600 | this.close();
601 | this.carry_on_completion();
602 |
603 | // event.codemirrorIgnore = true;
604 | // event._ipkmIgnore = true;
605 | // event.preventDefault();
606 | // this.pick();
607 | // this.close();
608 |
609 | } else if (code == keycodes.up || code == keycodes.down) {
610 | // need to do that to be able to move the arrow
611 | // when on the first or last line ofo a code cell
612 | event.codemirrorIgnore = true;
613 | event._ipkmIgnore = true;
614 | event.preventDefault();
615 |
616 | optionsLen = this.raw_result.length;
617 | index = this.selIndex;
618 | if (code == keycodes.up) {
619 | index--;
620 | }
621 | if (code == keycodes.down) {
622 | index++;
623 | }
624 | index = Math.min(Math.max(index, 0), optionsLen-1);
625 | this.selectNew(index);
626 | } else if (code == keycodes.pageup || code == keycodes.pagedown) {
627 | event.codemirrorIgnore = true;
628 | event._ipkmIgnore = true;
629 |
630 | optionsLen = this.raw_result.length;
631 | index = this.selIndex;
632 | if (code == keycodes.pageup) {
633 | index -= 10; // As 10 is the hard coded size of the drop down menu
634 | } else {
635 | index += 10;
636 | }
637 | index = Math.min(Math.max(index, 0), optionsLen-1);
638 | this.selectNew(index);
639 | } else if (code == keycodes.left || code == keycodes.right) {
640 | this.close();
641 | }
642 | };
643 |
644 | function _isCompletionKey(key) {
645 | return /^[A-Z0-9.:"]$/i.test(key);
646 | }
647 |
648 | function _processCompletionOnChange(cm, changes, completer) {
649 | var close_completion = false
650 | for (var i = 0; i < changes.length; ++i) {
651 | var change = changes[i];
652 | if (change.origin === "+input") {
653 | for (var j = 0; j < change.text.length; ++j) {
654 | var t = change.text[j];
655 | for (var k = 0; k < t.length; ++k) {
656 | if (_isCompletionKey(t[k])) {
657 | completer.startCompletion(false);
658 | return;
659 | }
660 | }
661 | }
662 | } else {
663 | var line = change.from.line;
664 | var ch = change.from.ch;
665 | if (ch === 0) continue;
666 | var removed = change.removed;
667 | if (removed.length > 1 || removed[0].length > 0) {
668 | var prevChar = cm.getRange({line: line, ch: ch - 1}, change.from);
669 | if (_isCompletionKey(prevChar)) {
670 | completer.startCompletion(false);
671 | return;
672 | }
673 | else close_completion = true;
674 | }
675 | }
676 | }
677 | if (close_completion) completer.close();
678 | }
679 |
680 | Completer.prototype.keypress = function (event) {
681 | /**
682 | * FIXME: This is a band-aid.
683 | * on keypress, trigger insertion of a single character.
684 | * This simulates the old behavior of completion as you type,
685 | * before events were disconnected and CodeMirror stopped
686 | * receiving events while the completer is focused.
687 | */
688 |
689 | var that = this;
690 | var code = event.keyCode;
691 |
692 | // don't handle keypress if it's not a character (arrows on FF)
693 | // or ENTER/TAB/BACKSPACE
694 | if (event.charCode === 0 ||
695 | code == keycodes.tab ||
696 | code == keycodes.enter ||
697 | code == keycodes.backspace
698 | ) return;
699 |
700 | if (_isCompletionKey(event.key))
701 | return;
702 |
703 | // this.close();
704 | this.editor.focus();
705 |
706 | setTimeout(function () {
707 | that.carry_on_completion();
708 | }, 10);
709 | };
710 |
711 | var EMPTY_ERRORS_RESULT = [[], []];
712 |
713 | CodeCell.prototype.findErrorsAtPos = function(pos) {
714 | if (pos.outside || Math.abs(pos.xRel) > 10)
715 | return EMPTY_ERRORS_RESULT;
716 |
717 | var ind = this.code_mirror.indexFromPos(pos);
718 | if (!this.errorsList)
719 | return EMPTY_ERRORS_RESULT;
720 |
721 | var filter = (er) => {
722 | if (!er.range) return false
723 | var er_start_ind = this.code_mirror.indexFromPos(er.range.start);
724 | var er_end_ind = this.code_mirror.indexFromPos(er.range.end);
725 | return er_start_ind <= ind && ind <= er_end_ind;
726 | };
727 | var passed = [];
728 | var other = [];
729 | this.errorsList.forEach((er) => {
730 | if (filter(er)) {
731 | passed.push(er);
732 | } else {
733 | other.push(er);
734 | }
735 | });
736 | return [passed, other];
737 | };
738 |
739 | function clearErrors(cell) {
740 | cell.code_mirror.getAllMarks()
741 | .filter(it => additionalClasses.some((cl) => it.className === cl))
742 | .forEach(it => it.clear());
743 | cell.errorsList = []
744 | }
745 |
746 | function clearAllErrors(notebook) {
747 | notebook.get_cells().forEach((cell) =>{
748 | if (cell.code_mirror) {
749 | clearErrors(cell);
750 | }
751 | });
752 | }
753 |
754 | CodeCell.prototype._handle_change = function(cm, changes) {
755 | _processCompletionOnChange(cm, changes, this.completer)
756 |
757 | clearAllErrors(this.notebook);
758 | this.kernel.listErrors(cm.getValue(), (msg) => {
759 | var content = msg.content;
760 | console.log(content);
761 |
762 | if(content.code !== cm.getValue()) {
763 | return;
764 | }
765 |
766 | var errors = content.errors;
767 | this.highlightErrors(errors);
768 | this.errorsList = errors;
769 |
770 | cm.scrollIntoView(null)
771 | });
772 | };
773 |
774 | CodeCell.prototype._handle_move = function(e) {
775 | var rect = e.currentTarget.getBoundingClientRect();
776 | var x = e.clientX - rect.left;
777 | var y = e.clientY - rect.top;
778 | var cursor_pos = this.code_mirror.coordsChar({left: x, top: y}, "local");
779 | var res = this.findErrorsAtPos(cursor_pos);
780 | var errors = res[0];
781 | var otherErrors = res[1];
782 | errors.forEach((error) => {
783 | tempTooltip(this.code_mirror, error, cursor_pos);
784 | });
785 | otherErrors.forEach((error) => {
786 | if (error.tip) {
787 | remove(error.tip);
788 | }
789 | })
790 | };
791 |
792 |
793 | CodeCell.prototype._isCompletionEvent = function(event, cur, editor) {
794 | if (event.type !== 'keydown' || event.ctrlKey || event.metaKey || event.altKey || !this.tooltip._hidden)
795 | return false;
796 | if (event.keyCode === keycodes.tab)
797 | return true;
798 | if (event.keyCode === keycodes.backspace){
799 | var pre_cursor = editor.getRange({line:0,ch:0},cur);
800 | return pre_cursor.length > 1 && _isCompletionKey(pre_cursor[pre_cursor.length - 2]);
801 | }
802 | return _isCompletionKey(event.key);
803 | };
804 |
805 | CodeCell.prototype.addEvent = function(obj, event, listener, bind_listener_name) {
806 | if (this[bind_listener_name]) {
807 | return;
808 | }
809 | var handler = this[bind_listener_name] = listener.bind(this);
810 | CodeMirror.off(obj, event, handler);
811 | CodeMirror.on(obj, event, handler);
812 | };
813 |
814 | CodeCell.prototype.bindEvents = function() {
815 | this.addEvent(this.code_mirror, 'changes', this._handle_change, "binded_handle_change");
816 | this.addEvent(this.code_mirror.display.lineSpace, 'mousemove', this._handle_move, 'binded_handle_move');
817 | };
818 |
819 | CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
820 | var that = this;
821 | this.bindEvents();
822 |
823 | // whatever key is pressed, first, cancel the tooltip request before
824 | // they are sent, and remove tooltip if any, except for tab again
825 | var tooltip_closed = null;
826 | if (event.type === 'keydown' && event.which !== keycodes.tab ) {
827 | tooltip_closed = this.tooltip.remove_and_cancel_tooltip();
828 | }
829 |
830 | var cur = editor.getCursor();
831 | if (event.keyCode === keycodes.enter){
832 | this.auto_highlight();
833 | }
834 |
835 | if (event.which === keycodes.down && event.type === 'keypress' && this.tooltip.time_before_tooltip >= 0) {
836 | // triger on keypress (!) otherwise inconsistent event.which depending on plateform
837 | // browser and keyboard layout !
838 | // Pressing '(' , request tooltip, don't forget to reappend it
839 | // The second argument says to hide the tooltip if the docstring
840 | // is actually empty
841 | this.tooltip.pending(that, true);
842 | } else if ( tooltip_closed && event.which === keycodes.esc && event.type === 'keydown') {
843 | // If tooltip is active, cancel it. The call to
844 | // remove_and_cancel_tooltip above doesn't pass, force=true.
845 | // Because of this it won't actually close the tooltip
846 | // if it is in sticky mode. Thus, we have to check again if it is open
847 | // and close it with force=true.
848 | if (!this.tooltip._hidden) {
849 | this.tooltip.remove_and_cancel_tooltip(true);
850 | }
851 | // If we closed the tooltip, don't let CM or the global handlers
852 | // handle this event.
853 | event.codemirrorIgnore = true;
854 | event._ipkmIgnore = true;
855 | event.preventDefault();
856 | return true;
857 | } else if (event.keyCode === keycodes.tab && event.type === 'keydown' && event.shiftKey) {
858 | if (editor.somethingSelected() || editor.getSelections().length !== 1){
859 | var anchor = editor.getCursor("anchor");
860 | var head = editor.getCursor("head");
861 | if( anchor.line !== head.line){
862 | return false;
863 | }
864 | }
865 | var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
866 | if (pre_cursor.trim() === "") {
867 | // Don't show tooltip if the part of the line before the cursor
868 | // is empty. In this case, let CodeMirror handle indentation.
869 | return false;
870 | }
871 | this.tooltip.request(that);
872 | event.codemirrorIgnore = true;
873 | event.preventDefault();
874 | return true;
875 | } else if (this._isCompletionEvent(event, cur, editor)) {
876 | // Tab completion.
877 | this.tooltip.remove_and_cancel_tooltip();
878 |
879 | // completion does not work on multicursor, it might be possible though in some cases
880 | if (editor.somethingSelected() || editor.getSelections().length > 1) {
881 | return false;
882 | }
883 | var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
884 | if (pre_cursor.trim() === "" && event.keyCode === keycodes.tab) {
885 | // Don't autocomplete if the part of the line before the cursor
886 | // is empty. In this case, let CodeMirror handle indentation.
887 | return false;
888 | } else {
889 | if (event.keyCode === keycodes.tab) {
890 | event.preventDefault();
891 | event.codemirrorIgnore = true;
892 | this.completer.startCompletion(true);
893 | }
894 | return true;
895 | }
896 | }
897 |
898 | // keyboard event wasn't one of those unique to code cells, let's see
899 | // if it's one of the generic ones (i.e. check edit mode shortcuts)
900 | return Cell.prototype.handle_codemirror_keyevent.apply(this, [editor, event]);
901 | };
902 |
903 | CodeCell.prototype._handle_execute_reply = function (msg) {
904 | clearAllErrors(this.notebook)
905 | this.bindEvents();
906 |
907 | this.set_input_prompt(msg.content.execution_count);
908 | this.element.removeClass("running");
909 | this.events.trigger('set_dirty.Notebook', {value: true});
910 |
911 | if (msg.content.status === 'error') {
912 | var addInfo = msg.content.additionalInfo || {};
913 | var from = {line: addInfo.lineStart, col: addInfo.colStart};
914 | var to;
915 | if (addInfo.lineEnd !== -1 && addInfo.colEnd !== -1) {
916 | to = {line: addInfo.lineEnd, col: addInfo.colEnd};
917 | } else {
918 | to = {line: from.line, col: from.col + 3};
919 | }
920 |
921 | if (from.line !== undefined && from.col !== undefined) {
922 | var message = addInfo.message;
923 | message = message.replace(/^\(\d+:\d+ - \d+\) /, "");
924 | message = message.replace(/^\(\d+:\d+\) - \(\d+:\d+\) /, "");
925 |
926 | this.errorsList = [
927 | {
928 | start: from,
929 | end: to,
930 | message: message,
931 | severity: "ERROR"
932 | }];
933 | this.highlightErrors(this.errorsList);
934 | }
935 | }
936 | };
937 |
938 | function tempTooltip(cm, error, pos) {
939 | if (cm.state.errorTip) remove(cm.state.errorTip);
940 | var where = cm.charCoords(pos);
941 | var tip = makeTooltip(where.right + 1, where.bottom, error.message, error.tip);
942 | error.tip = cm.state.errorTip = tip;
943 | fadeIn(tip);
944 |
945 | function maybeClear() {
946 | old = true;
947 | if (!mouseOnTip) clear();
948 | }
949 | function clear() {
950 | cm.state.ternTooltip = null;
951 | if (tip.parentNode) fadeOut(tip);
952 | clearActivity();
953 | }
954 | var mouseOnTip = false, old = false;
955 | CodeMirror.on(tip, "mousemove", function() { mouseOnTip = true; });
956 | CodeMirror.on(tip, "mouseout", function(e) {
957 | var related = e.relatedTarget || e.toElement;
958 | if (!related || !CodeMirror.contains(tip, related)) {
959 | if (old) clear();
960 | else mouseOnTip = false;
961 | }
962 | });
963 | setTimeout(maybeClear, 100000);
964 | var clearActivity = onEditorActivity(cm, clear)
965 | }
966 |
967 | function fadeIn(tooltip) {
968 | document.body.appendChild(tooltip);
969 | tooltip.style.opacity = "1";
970 | }
971 |
972 | function fadeOut(tooltip) {
973 | tooltip.style.opacity = "0";
974 | setTimeout(function() { remove(tooltip); }, 100);
975 | }
976 |
977 | function makeTooltip(x, y, content, element) {
978 | var node = element || elt("div", "kotlin-error-tooltip", content);
979 | node.style.left = x + "px";
980 | node.style.top = y + "px";
981 | return node;
982 | }
983 |
984 | function elt(tagname, cls /*, ... elts*/) {
985 | var e = document.createElement(tagname);
986 | if (cls) e.className = cls;
987 | for (var i = 2; i < arguments.length; ++i) {
988 | var elt = arguments[i];
989 | if (typeof elt == "string") elt = document.createTextNode(elt);
990 | e.appendChild(elt);
991 | }
992 | return e;
993 | }
994 |
995 | function remove(node) {
996 | var p = node && node.parentNode;
997 | if (p) p.removeChild(node);
998 | }
999 |
1000 | function onEditorActivity(cm, f) {
1001 | cm.on("cursorActivity", f);
1002 | cm.on("blur", f);
1003 | cm.on("scroll", f);
1004 | cm.on("setDoc", f);
1005 | return function() {
1006 | cm.off("cursorActivity", f);
1007 | cm.off("blur", f);
1008 | cm.off("scroll", f);
1009 | cm.off("setDoc", f);
1010 | }
1011 | }
1012 |
1013 | Kernel.prototype.listErrors = function (code, callback) {
1014 | var callbacks;
1015 | if (callback) {
1016 | callbacks = { shell : { reply : callback } };
1017 | }
1018 | var content = {
1019 | code : code
1020 | };
1021 | return this.send_shell_message("list_errors_request", content, callbacks);
1022 | };
1023 |
1024 | }
1025 |
1026 | return {onload: onload};
1027 |
1028 | });
--------------------------------------------------------------------------------
/ghidra_jupyter/kernel/kernel.json:
--------------------------------------------------------------------------------
1 | {
2 | "display_name": "Ghidra(Kotlin)",
3 | "language": "kotlin",
4 | "interrupt_mode": "message",
5 | "argv": [
6 | "python",
7 | "-m",
8 | "ghidra_jupyter.dispatcher",
9 | "{connection_file}"
10 | ]
11 | }
--------------------------------------------------------------------------------
/ghidra_jupyter/kernel/logo-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/ghidra_jupyter/kernel/logo-16x16.png
--------------------------------------------------------------------------------
/ghidra_jupyter/kernel/logo-64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/ghidra_jupyter/kernel/logo-64x64.png
--------------------------------------------------------------------------------
/ghidra_jupyter/kernel/wavyline-orange.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/ghidra_jupyter/kernel/wavyline-orange.gif
--------------------------------------------------------------------------------
/ghidra_jupyter/kernel/wavyline-red.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/ghidra_jupyter/kernel/wavyline-red.gif
--------------------------------------------------------------------------------
/ghidra_jupyter/requirements.txt:
--------------------------------------------------------------------------------
1 | psutil
2 | attrs
3 | notebook
4 | tqdm
5 | requests
--------------------------------------------------------------------------------
/ghidra_jupyter/setup.py:
--------------------------------------------------------------------------------
1 | import glob
2 |
3 | from setuptools import find_packages, setup
4 |
5 | GITHUB_URL = "https://github.com/GhidraJupyter/ghidra-jupyter-kotlin"
6 |
7 | LONG_DESCRIPTION = f"""
8 | # Ghidra-Jupyter
9 |
10 | A Jupyter kernel (notebook & QtConsole) plugin for Ghidra.
11 |
12 | Currently supporting [Kotlin-Jupyter](https://github.com/Kotlin/kotlin-jupyter).
13 |
14 | For info and installation see the [github repo]({GITHUB_URL}).
15 | """
16 |
17 |
18 | def is_requirement(line):
19 | return not line.startswith("-e") and not line.startswith(".")
20 |
21 |
22 | def get_requirements(extra=None):
23 | if extra:
24 | filename = f"requirements.{extra}.txt"
25 | else:
26 | filename = "requirements.txt"
27 |
28 | with open(filename) as f:
29 | return [line for line in f.read().splitlines() if is_requirement(line)]
30 |
31 |
32 | DATA_FILES = [
33 | ("share/jupyter/kernels/ghidra-kotlin", glob.glob("kernel/*")),
34 | ]
35 |
36 |
37 | PACKAGES = find_packages(where="src")
38 | INSTALL_REQUIRES = get_requirements()
39 | setup(
40 | name="ghidra_jupyter",
41 | version="1.1.0",
42 | packages=PACKAGES,
43 | package_dir={"": "src"},
44 | include_package_data=True,
45 | url=GITHUB_URL,
46 | license="MIT License",
47 | author="GhidraJupyter",
48 | author_email="",
49 | description="A Jupyter kernel for Ghidra",
50 | long_description_content_type="text/markdown",
51 | long_description=LONG_DESCRIPTION,
52 | classifiers=[
53 | "Programming Language :: Python :: 3",
54 | "License :: OSI Approved :: MIT License",
55 | "Operating System :: OS Independent",
56 | ],
57 | install_requires=INSTALL_REQUIRES,
58 | entry_points={
59 | "console_scripts": [
60 | "ghidra-jupyter=ghidra_jupyter.installer:main",
61 | ],
62 | },
63 | data_files=DATA_FILES,
64 | python_requires=">=3.6",
65 | )
66 |
--------------------------------------------------------------------------------
/ghidra_jupyter/src/ghidra_jupyter/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/ghidra_jupyter/src/ghidra_jupyter/__init__.py
--------------------------------------------------------------------------------
/ghidra_jupyter/src/ghidra_jupyter/dispatcher.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 | import psutil
4 | import attr
5 |
6 | PROXY_ENV_VAR = "GHIDRA_JUPYTER_PROXY"
7 |
8 |
9 | @attr.s(auto_attribs=True, frozen=True, slots=True)
10 | class ProxyPaths:
11 | pid: str
12 | path: str
13 |
14 |
15 | def get_proxy_paths():
16 | base = os.environ.get(PROXY_ENV_VAR)
17 | if not base:
18 | base = os.path.join(os.path.expanduser("~"), ".ghidra", "notebook_proxy")
19 |
20 | return ProxyPaths(
21 | pid=base + ".pid",
22 | path=base + ".path",
23 | )
24 |
25 |
26 | def main():
27 | print("starting!")
28 | proxy_paths = get_proxy_paths()
29 |
30 | with open(proxy_paths.path, "w") as f:
31 | f.write(sys.argv[1])
32 |
33 | with open(proxy_paths.pid, "r") as f:
34 | pid = int(f.read().strip())
35 |
36 | psutil.Process(pid=pid).wait()
37 |
38 |
39 | if __name__ == "__main__":
40 | main()
41 |
--------------------------------------------------------------------------------
/ghidra_jupyter/src/ghidra_jupyter/installer.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import os
3 | import re
4 | import shutil
5 | import zipfile
6 | from tempfile import TemporaryDirectory
7 | from typing import Optional, Tuple
8 | from packaging import version
9 | from pathlib import Path
10 |
11 | import requests
12 | from tqdm import tqdm
13 |
14 | REPO = "GhidraJupyter/ghidra-jupyter-kotlin"
15 | # NAME_PATTERN = r"GhidraJupyterKotlin[v0-9.\-_]*\.zip"
16 | NAME_PATTERN = r"ghidra_(.+)_PUBLIC_(\d{8})_GhidraJupyterKotlin.zip"
17 |
18 | def download_file(url: str, path: str):
19 | with requests.get(url, stream=True) as response:
20 | response.raise_for_status()
21 |
22 | total_size_in_bytes = int(response.headers.get("content-length", 0)) or None
23 | block_size = 1024 # 1 Kibibyte
24 |
25 | with open(path, "wb") as f, tqdm(
26 | total=total_size_in_bytes, unit="iB", unit_scale=True
27 | ) as progress_bar:
28 | for chunk in response.iter_content(chunk_size=block_size):
29 | f.write(chunk)
30 | progress_bar.update(len(chunk))
31 |
32 |
33 | def _install_from_path(ghidra_install_dir: str, extension_path: str):
34 | # First, remove the extension if it already exists
35 | install_path = os.path.join(
36 | ghidra_install_dir, "Ghidra", "Extensions", "GhidraJupyterKotlin"
37 | )
38 | print(f"Installing extension to {install_path}")
39 | shutil.rmtree(
40 | install_path,
41 | ignore_errors=True,
42 | )
43 |
44 | with zipfile.ZipFile(extension_path) as zip_ref:
45 | zip_ref.extractall(os.path.join(ghidra_install_dir, "Ghidra", "Extensions"))
46 |
47 |
48 | def _get_ghidra_dir(ghidra_install_dir: Optional[str]) -> str:
49 | return ghidra_install_dir or os.environ.get("GHIDRA_INSTALL_DIR")
50 |
51 |
52 | def _get_ghidra_version(ghidra_install_dir: Optional[str]) -> version.Version:
53 | app_properties = os.path.join(ghidra_install_dir, "Ghidra", "application.properties")
54 | with open(app_properties, "r") as f:
55 | for line in f.readlines():
56 | prop_split = line.split("=")
57 | if len(prop_split) != 2:
58 | continue
59 | key, value = prop_split
60 | if key == "application.version":
61 | return version.parse(value)
62 |
63 |
64 | def get_download_url(repo, name_pattern) -> Tuple[str, version.Version]:
65 | release = requests.get(f"https://api.github.com/repos/{repo}/releases/latest")
66 | release.raise_for_status()
67 | for asset in release.json()["assets"]:
68 | m = re.match(name_pattern, asset["name"])
69 | if m:
70 | extension_version = version.parse(m.group(1))
71 | return asset["browser_download_url"], extension_version
72 |
73 |
74 |
75 | def install_extension(
76 | ghidra_install_dir: Optional[str],
77 | extension_path: Optional[str],
78 | extension_url: Optional[str],
79 | ):
80 | ghidra_install_dir = _get_ghidra_dir(ghidra_install_dir)
81 | ghidra_version = _get_ghidra_version(ghidra_install_dir)
82 |
83 | if not ghidra_install_dir:
84 | print("Missing $GHIDRA_INSTALL_DIR")
85 | return
86 |
87 | if extension_path:
88 | _install_from_path(ghidra_install_dir, extension_path)
89 |
90 | else:
91 | with TemporaryDirectory() as tempdir:
92 | extension_path = os.path.join(tempdir, "Extension.zip")
93 | if extension_url is None:
94 | extension_url, extension_version = get_download_url(REPO, NAME_PATTERN)
95 | print("Detected Ghidra Version: ", ghidra_version)
96 | print("Extension Version: ", extension_version)
97 | if extension_version.major != ghidra_version.major:
98 | print("ERROR: Major version of Ghidra (%s) and Extension (%s) don't match, refusing to install" %
99 | (ghidra_version, extension_version))
100 | return
101 | elif ghidra_version > extension_version:
102 | print("WARNING: Your Ghidra version is newer than the extension version")
103 | print("There could be some unresolved compatibility issue or we forgot to bump the CI version")
104 | print("Please check https://github.com/%s" % REPO)
105 | elif extension_version > ghidra_version:
106 | print("!WARNING! " * 10)
107 | print("WARNING: Ghidra Version is %s, but extension_version is %s"
108 | % (ghidra_version, extension_version))
109 | print("WARNING: Extension will still be installed, but might encounter unpredictable issues. "
110 | "Please update your Ghidra install or manually install an older release")
111 | print("!WARNING! " * 10)
112 |
113 | print(f"Downloading Ghidra extension from {extension_url}")
114 | download_file(extension_url, extension_path)
115 | print("Download complete.")
116 | _install_from_path(ghidra_install_dir, extension_path)
117 |
118 | print("Installation Complete.")
119 |
120 |
121 | def remove_extension(ghidra_install_dir: Optional[str]):
122 | ghidra_install_dir = _get_ghidra_dir(ghidra_install_dir)
123 | if not ghidra_install_dir:
124 | print("Missing $GHIDRA_INSTALL_DIR")
125 | return
126 |
127 | install_path = os.path.join(
128 | ghidra_install_dir, "Ghidra", "Extensions", "GhidraJupyterKotlin"
129 | )
130 | print(f"Removing extension at {install_path}")
131 | shutil.rmtree(
132 | install_path,
133 | ignore_errors=True,
134 | )
135 |
136 | print("Extension removed.")
137 |
138 |
139 | def create_parser():
140 | parser = argparse.ArgumentParser()
141 | subparsers = parser.add_subparsers(dest="command")
142 |
143 | install_extension = subparsers.add_parser("install-extension")
144 | install_extension.add_argument(
145 | "--ghidra",
146 | nargs="?",
147 | help="Ghidra install directory. Defaults to $GHIDRA_INSTALL_DIR",
148 | )
149 | install_extension.add_argument(
150 | "--extension-path",
151 | nargs="?",
152 | help="Path to a local .zip of the extension",
153 | )
154 | install_extension.add_argument(
155 | "--extension-url",
156 | nargs="?",
157 | help="URL to download the extension from",
158 | )
159 |
160 | remove_extension = subparsers.add_parser("remove-extension")
161 | remove_extension.add_argument(
162 | "--ghidra",
163 | nargs="?",
164 | help="Ghidra install directory. Defaults to $GHIDRA_INSTALL_DIR",
165 | )
166 |
167 | return parser
168 |
169 |
170 | def main():
171 | parser = create_parser()
172 | args = parser.parse_args()
173 |
174 | if args.command == "install-extension":
175 | install_extension(args.ghidra, args.extension_path, args.extension_url)
176 |
177 | elif args.command == "remove-extension":
178 | remove_extension(args.ghidra)
179 |
180 |
181 | if __name__ == "__main__":
182 | main()
183 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | GRADLE_MIN=1.0
2 | GRADLE_MAX=100.0
--------------------------------------------------------------------------------
/resources/notebook-button.svg:
--------------------------------------------------------------------------------
1 |
2 |
468 |
--------------------------------------------------------------------------------
/resources/options.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/resources/options.png
--------------------------------------------------------------------------------
/resources/qtconsole-button.svg:
--------------------------------------------------------------------------------
1 |
2 |
537 |
--------------------------------------------------------------------------------
/resources/readme/buttons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/resources/readme/buttons.png
--------------------------------------------------------------------------------
/resources/readme/create_notebook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/resources/readme/create_notebook.png
--------------------------------------------------------------------------------
/resources/readme/interrupt_demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/resources/readme/interrupt_demo.png
--------------------------------------------------------------------------------
/resources/readme/menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/resources/readme/menu.png
--------------------------------------------------------------------------------
/resources/readme/notebook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/resources/readme/notebook.png
--------------------------------------------------------------------------------
/resources/readme/notebook_view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/resources/readme/notebook_view.png
--------------------------------------------------------------------------------
/resources/readme/qtconsole.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/resources/readme/qtconsole.png
--------------------------------------------------------------------------------
/resources/readme/qtconsole_window.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/resources/readme/qtconsole_window.png
--------------------------------------------------------------------------------
/resources/readme/waiting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GhidraJupyter/ghidra-jupyter-kotlin/dcbd916a239608633e3d281a56c22995f9176b00/resources/readme/waiting.png
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ":GhidraJupyterKotlin"
2 |
--------------------------------------------------------------------------------